Environment
- OpenPets version: v4.0.0 (Linux .deb)
- Desktop: KDE Plasma, X11 session
- Dual monitor setup: 2x 1920x1080 @ 100% scale (no fractional scaling), one monitor at x=0, the other at x=1920
Bug
Clicking into the pet's chat bubble message input ("Message your pet...") does not transfer real keyboard focus to the OpenPets window. Anything typed afterward goes to whatever window previously held focus (e.g. a terminal) instead of appearing in the chat input.
Steps to reproduce
- Open the pet's chat bubble (tried both the small "compact" popup and the full expanded chat window).
- Click directly into the message input field.
- Type text.
- Nothing appears in the input; keystrokes land in the previously-focused window instead.
Diagnostics performed
- Confirmed with
xdotool getwindowfocus, polled continuously (~every 0.4s) while clicking the input: real X11 keyboard focus never transfers to the OpenPets window at any point.
- OpenPets' own
openpets.log shows focus policy applied ... focusable=true hasInteractiveInput=true being set correctly when the chat opens/expands, but this internal state does not correspond to actual X11 input focus.
- The pet/chat window never appears in the Alt+Tab task switcher (
skipTaskbar: true), and clicking it produces no visible reaction at all (no highlight, no raise/bring-to-front).
- Ruled out KDE's "Focus stealing prevention" setting (tested with it set to None — no change).
- Ruled out window position relative to the monitor boundary (moved the window fully onto one monitor, away from the x=1920 seam — no change).
- Ruled out display scaling mismatch (both monitors at 100%,
QT_AUTO_SCREEN_SCALE_FACTOR=0).
- Ruled out input-method issues (ibus is running normally).
Root cause hypothesis (from source)
In apps/desktop/src/wayland-backend.ts:
export function shouldPetWindowBeFocusable(
platform: NodeJS.Platform | string,
_effectiveWaylandBackend: boolean,
hasInteractiveInput = false,
): boolean {
if (hasInteractiveInput) return true;
return platform !== "linux";
}
On Linux this returns false at window-creation time (no interactive input yet), so pet-window.ts's createBasePetWindowWithMode() constructs the BrowserWindow with focusable: false baked into the initial X11 window hints:
const focusable = shouldPetWindowBeFocusable(process.platform, effectiveWaylandBackend, focusOptions.hasInteractiveInput === true);
const window = new BrowserWindow({
...
focusable,
...
});
When the chat bubble opens, applyPetWindowFocusPolicy() (pet-window.ts) and setCarrierMode() (default-pet-chat.ts) call window.setFocusable(true) + window.focus() on the already-mapped window to flip it back to focusable.
This looks like the bug: on X11 (observed here on KDE/KWin), a window created with focusable: false gets WM_HINTS.input = False at map time. Electron's setFocusable(true) afterward updates the property, but some window managers (KWin apparently included) don't re-evaluate focus-acceptance for a window that already told them "no input" once mapped — so window.focus() becomes a no-op even though Electron's own internal state (and the app's debug log) reports success. That matches exactly what I observed: the log always claims focus was granted, but xdotool getwindowfocus shows it never actually was.
There is also a smaller ordering race in setCarrierMode() (default-pet-chat.ts):
if (nextSize && !previousSize) {
window.setBounds(nextBounds, false);
window.setFocusable(true);
window.focus(); // fires before the Linux input-shape is widened
}
...
if (isExpanded) {
window.setFocusable(true);
window.focus();
}
void import("./pet-window.js").then(({ applyLinuxPetWindowShapeWithExpansion, refreshDefaultPetFocusPolicy }) => {
applyLinuxPetWindowShapeWithExpansion(window, isExpanded, compactIsOpen, activeChatPanelHeight);
refreshDefaultPetFocusPolicy(window);
}).catch(() => {});
focus() runs synchronously, but the X11 setShape() call that widens the window's clickable/input region to cover the new chat panel only happens afterward, inside a dynamically-imported async callback. If the WM validates a focus/activate request against the window's current input region, this ordering could independently drop the focus grab even if the "created non-focusable" issue above were fixed.
Suggested fix
- Always create the Linux pet window with
focusable: true, and rely solely on setShape() (already used for hit-testing) to make the idle/passive pet area click-through, instead of also toggling the OS-level focusable flag. This avoids ever asking KWin (or other WMs with the same caching behavior) to revive focus-acceptance on an already-mapped window. shouldPetWindowBeFocusable could simply return true for X11; Wayland/layer-shell backends already avoid focus through a separate path (isLayerShellBackendRequested) and would be unaffected.
- If non-focusable-at-creation needs to stay (e.g. for Wayland compositors like Niri, per the comment above
shouldPetWindowBeFocusable), at least fix the ordering in setCarrierMode(): hoist the dynamic import("./pet-window.js") to a static import and apply applyLinuxPetWindowShapeWithExpansion() before calling window.setFocusable(true) / window.focus(), so the WM sees the final input region before the focus request arrives.
- Add a regression check that verifies real WM-level focus transfer on Linux (e.g. via
xdotool getwindowfocus in manual QA or CI under X11), not just Electron's own isFocusable()/internal flag — the current debug logging only reflects Electron's belief, which is how this class of bug can ship silently.
I'm happy to send a PR for this if useful — will follow up.
Environment
Bug
Clicking into the pet's chat bubble message input ("Message your pet...") does not transfer real keyboard focus to the OpenPets window. Anything typed afterward goes to whatever window previously held focus (e.g. a terminal) instead of appearing in the chat input.
Steps to reproduce
Diagnostics performed
xdotool getwindowfocus, polled continuously (~every 0.4s) while clicking the input: real X11 keyboard focus never transfers to the OpenPets window at any point.openpets.logshowsfocus policy applied ... focusable=true hasInteractiveInput=truebeing set correctly when the chat opens/expands, but this internal state does not correspond to actual X11 input focus.skipTaskbar: true), and clicking it produces no visible reaction at all (no highlight, no raise/bring-to-front).QT_AUTO_SCREEN_SCALE_FACTOR=0).Root cause hypothesis (from source)
In
apps/desktop/src/wayland-backend.ts:On Linux this returns
falseat window-creation time (no interactive input yet), sopet-window.ts'screateBasePetWindowWithMode()constructs theBrowserWindowwithfocusable: falsebaked into the initial X11 window hints:When the chat bubble opens,
applyPetWindowFocusPolicy()(pet-window.ts) andsetCarrierMode()(default-pet-chat.ts) callwindow.setFocusable(true)+window.focus()on the already-mapped window to flip it back to focusable.This looks like the bug: on X11 (observed here on KDE/KWin), a window created with
focusable: falsegetsWM_HINTS.input = Falseat map time. Electron'ssetFocusable(true)afterward updates the property, but some window managers (KWin apparently included) don't re-evaluate focus-acceptance for a window that already told them "no input" once mapped — sowindow.focus()becomes a no-op even though Electron's own internal state (and the app's debug log) reports success. That matches exactly what I observed: the log always claims focus was granted, butxdotool getwindowfocusshows it never actually was.There is also a smaller ordering race in
setCarrierMode()(default-pet-chat.ts):focus()runs synchronously, but the X11setShape()call that widens the window's clickable/input region to cover the new chat panel only happens afterward, inside a dynamically-imported async callback. If the WM validates a focus/activate request against the window's current input region, this ordering could independently drop the focus grab even if the "created non-focusable" issue above were fixed.Suggested fix
focusable: true, and rely solely onsetShape()(already used for hit-testing) to make the idle/passive pet area click-through, instead of also toggling the OS-level focusable flag. This avoids ever asking KWin (or other WMs with the same caching behavior) to revive focus-acceptance on an already-mapped window.shouldPetWindowBeFocusablecould simplyreturn truefor X11; Wayland/layer-shell backends already avoid focus through a separate path (isLayerShellBackendRequested) and would be unaffected.shouldPetWindowBeFocusable), at least fix the ordering insetCarrierMode(): hoist the dynamicimport("./pet-window.js")to a static import and applyapplyLinuxPetWindowShapeWithExpansion()before callingwindow.setFocusable(true)/window.focus(), so the WM sees the final input region before the focus request arrives.xdotool getwindowfocusin manual QA or CI under X11), not just Electron's ownisFocusable()/internal flag — the current debug logging only reflects Electron's belief, which is how this class of bug can ship silently.I'm happy to send a PR for this if useful — will follow up.