From 62b4fd21ff11ee59376d0502c1cfa22b22b08736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9C=A4=EC=A7=84=28Lee=20Yunjin=29?= <168180007+gg582@users.noreply.github.com> Date: Thu, 27 Aug 2026 18:25:06 +0900 Subject: [PATCH] fix(stage-tamagotchi): fix window visibility and ozone flags on wayland (#2288) --- .gitignore | 2 + .../ai.moeru.airi.flatpak.yml | 6 ++ .../src/main/app/ozone.test.ts | 90 +++++++++++++++++++ apps/stage-tamagotchi/src/main/app/ozone.ts | 21 +++++ apps/stage-tamagotchi/src/main/index.ts | 55 +++++++++--- .../src/main/services/electron/window.ts | 9 +- .../src/main/windows/caption/index.ts | 8 +- .../src/main/windows/main/index.ts | 8 +- .../src/main/windows/shared/display.test.ts | 16 ++++ .../src/main/windows/shared/display.ts | 12 ++- .../src/main/windows/shared/index.ts | 2 +- .../src/main/windows/shared/window.test.ts | 83 +++++++++++++++++ .../src/main/windows/shared/window.ts | 26 +++++- .../src/main/windows/widgets/index.ts | 17 +--- eslint.config.ts | 2 + .../src/skills/actions/world-interactions.ts | 9 +- integrations/minecraft/src/skills/blocks.ts | 9 +- 17 files changed, 330 insertions(+), 45 deletions(-) create mode 100644 apps/stage-tamagotchi/src/main/app/ozone.test.ts create mode 100644 apps/stage-tamagotchi/src/main/app/ozone.ts create mode 100644 apps/stage-tamagotchi/src/main/windows/shared/window.test.ts diff --git a/.gitignore b/.gitignore index 749acfb01..dbd8070fc 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,8 @@ coverage/ # Build out/ bundle/ +flatpak/ +flatpak-repo/ .flatpak-builder/ .flatpak-repo/ *.flatpak diff --git a/apps/stage-tamagotchi/ai.moeru.airi.flatpak.yml b/apps/stage-tamagotchi/ai.moeru.airi.flatpak.yml index c6880ddb8..0d526af3e 100644 --- a/apps/stage-tamagotchi/ai.moeru.airi.flatpak.yml +++ b/apps/stage-tamagotchi/ai.moeru.airi.flatpak.yml @@ -10,7 +10,9 @@ finish-args: # GUI environment - --share=network - --socket=x11 + - --socket=fallback-x11 - --socket=wayland + - --device=dri - --device=all - --socket=pulseaudio - --socket=system-bus @@ -94,4 +96,8 @@ modules: commands: - ulimit -c 0 - export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID" + - | + if [ -n "$WAYLAND_DISPLAY" ]; then + export ELECTRON_OZONE_PLATFORM_HINT=auto + fi - exec zypak-wrapper /app/lib/airi/airi "$@" diff --git a/apps/stage-tamagotchi/src/main/app/ozone.test.ts b/apps/stage-tamagotchi/src/main/app/ozone.test.ts new file mode 100644 index 000000000..664e9d584 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/app/ozone.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from 'vitest' + +import { resolveIsWayland } from './ozone' + +describe('resolveIsWayland', () => { + it('resolves to true when --ozone-platform is explicitly wayland', () => { + expect(resolveIsWayland({ + explicitOzonePlatform: 'wayland', + env: {}, + })).toBe(true) + }) + + it('resolves to false when --ozone-platform is explicitly x11 even in Wayland environment', () => { + expect(resolveIsWayland({ + explicitOzonePlatform: 'x11', + env: { + WAYLAND_DISPLAY: 'wayland-0', + XDG_SESSION_TYPE: 'wayland', + }, + })).toBe(false) + }) + + it('treats --ozone-platform=auto as unresolved and falls back to environment', () => { + expect(resolveIsWayland({ + explicitOzonePlatform: 'auto', + env: { + WAYLAND_DISPLAY: 'wayland-0', + }, + })).toBe(true) + + expect(resolveIsWayland({ + explicitOzonePlatform: 'auto', + env: { + XDG_SESSION_TYPE: 'x11', + }, + })).toBe(false) + }) + + it('resolves based on --ozone-platform-hint when not auto', () => { + expect(resolveIsWayland({ + ozonePlatformHint: 'wayland', + env: {}, + })).toBe(true) + + expect(resolveIsWayland({ + ozonePlatformHint: 'x11', + env: { + WAYLAND_DISPLAY: 'wayland-0', + }, + })).toBe(false) + }) + + it('treats --ozone-platform-hint=auto as unresolved and falls back to environment', () => { + expect(resolveIsWayland({ + ozonePlatformHint: 'auto', + env: { + WAYLAND_DISPLAY: 'wayland-0', + }, + })).toBe(true) + + expect(resolveIsWayland({ + ozonePlatformHint: 'auto', + env: {}, + })).toBe(false) + }) + + it('falls back to environment variables when no flags are present', () => { + expect(resolveIsWayland({ + env: { + WAYLAND_DISPLAY: 'wayland-0', + }, + })).toBe(true) + + expect(resolveIsWayland({ + env: { + XDG_SESSION_TYPE: 'wayland', + }, + })).toBe(true) + + expect(resolveIsWayland({ + env: { + XDG_SESSION_TYPE: 'x11', + }, + })).toBe(false) + + expect(resolveIsWayland({ + env: {}, + })).toBe(false) + }) +}) diff --git a/apps/stage-tamagotchi/src/main/app/ozone.ts b/apps/stage-tamagotchi/src/main/app/ozone.ts new file mode 100644 index 000000000..fe56ebbab --- /dev/null +++ b/apps/stage-tamagotchi/src/main/app/ozone.ts @@ -0,0 +1,21 @@ +/** + * Resolves whether the application is running under the Wayland Ozone backend. + * + * Checks explicit command-line switches before falling back to session environment variables. + * Treats 'auto' as an unresolved platform selection and resolves it using session environment variables. + */ +export function resolveIsWayland(params: { + explicitOzonePlatform?: string + ozonePlatformHint?: string + env?: Record +}): boolean { + if (params.explicitOzonePlatform && params.explicitOzonePlatform !== 'auto') { + return params.explicitOzonePlatform === 'wayland' + } + + if (params.ozonePlatformHint && params.ozonePlatformHint !== 'auto') { + return params.ozonePlatformHint === 'wayland' + } + + return Boolean(params.env?.WAYLAND_DISPLAY || params.env?.XDG_SESSION_TYPE === 'wayland') +} diff --git a/apps/stage-tamagotchi/src/main/index.ts b/apps/stage-tamagotchi/src/main/index.ts index 47f9437e9..aea8fc897 100644 --- a/apps/stage-tamagotchi/src/main/index.ts +++ b/apps/stage-tamagotchi/src/main/index.ts @@ -22,6 +22,7 @@ import icon from '../../resources/icon.png?asset' import { openDebugger, setupDebugger } from './app/debugger' import { nullFileLoggerHandle, setupFileLogger } from './app/file-logger' +import { resolveIsWayland } from './app/ozone' import { installSingleInstanceGuard } from './app/single-instance' import { createArtistryConfig } from './configs/artistry' import { createGlobalAppConfig } from './configs/global' @@ -79,21 +80,53 @@ if (appUserDataPath) { // https://github.com/electron/electron/issues/41763#issuecomment-2051725363 // https://github.com/electron/electron/issues/41763#issuecomment-3143338995 if (isLinux) { - app.commandLine.appendSwitch('enable-features', 'SharedArrayBuffer') + // NOTICE: + // All enabled features must be joined into a single comma-separated string + // instead of calling appendSwitch('enable-features', ...) once per feature. + // Root cause: Chromium's commandLine stores switches by key, so each + // appendSwitch('enable-features', ...) call overwrites the previous value and + // only the last feature survives. + // Source: Chromium base::CommandLine behavior; see + // https://github.com/electron/electron/issues/41763 for the WebGPU setup this supports. + // Removal condition: never for the join itself; this block can be deleted once + // WebGPU works on Linux Electron without manual feature switches. + const enabledFeatures = [ + 'SharedArrayBuffer', + ] + app.commandLine.appendSwitch('enable-unsafe-webgpu') - app.commandLine.appendSwitch('enable-features', 'Vulkan') - // NOTICE: we need UseOzonePlatform, WaylandWindowDecorations for working on Wayland. - // Partially related to https://github.com/electron/electron/issues/41551, since X11 is deprecating now, - // we can safely remove the feature flags for Electron once they made it default supported. - // Fixes: https://github.com/moeru-ai/airi/issues/757 - // Ref: https://github.com/mmaura/poe2linuxcompanion/blob/90664607a147ea5ccea28df6139bd95fb0ebab0e/electron/main/index.ts#L28-L46 - if (env.XDG_SESSION_TYPE === 'wayland') { - app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal') + // Check explicit command-line switches before falling back to session environment variables. + // When running with XWayland (e.g. '--ozone-platform=x11'), session variables like WAYLAND_DISPLAY + // are still inherited from the Wayland desktop, but Chromium uses the explicitly specified Ozone backend. + // Treat explicit 'auto' as an unresolved platform selection and resolve using session environment variables. + const isWayland = resolveIsWayland({ + explicitOzonePlatform: app.commandLine.getSwitchValue('ozone-platform'), + ozonePlatformHint: app.commandLine.getSwitchValue('ozone-platform-hint'), + env, + }) - app.commandLine.appendSwitch('enable-features', 'UseOzonePlatform') - app.commandLine.appendSwitch('enable-features', 'WaylandWindowDecorations') + if (isWayland) { + enabledFeatures.push('GlobalShortcutsPortal', 'UseOzonePlatform', 'WaylandWindowDecorations') + if (!app.commandLine.hasSwitch('ozone-platform-hint')) { + app.commandLine.appendSwitch('ozone-platform-hint', 'auto') + } } + else { + // NOTICE: + // Vulkan must only be enabled on non-Wayland sessions, otherwise GPU + // initialization fails or rendering glitches appear. + // Root cause: Vulkan is incompatible with '--ozone-platform=wayland' in + // Chromium's surface factory; the Wayland Ozone backend cannot present + // Vulkan surfaces. + // Source: Chromium Ozone/Wayland surface factory; workaround tracked via + // https://github.com/electron/electron/issues/41763 (WebGPU on Linux). + // Removal condition: when Chromium/Electron supports Vulkan with the Wayland + // Ozone backend, drop the isWayland guard and always push 'Vulkan'. + enabledFeatures.push('Vulkan') + } + + app.commandLine.appendSwitch('enable-features', enabledFeatures.join(',')) } app.dock?.setIcon(icon) diff --git a/apps/stage-tamagotchi/src/main/services/electron/window.ts b/apps/stage-tamagotchi/src/main/services/electron/window.ts index ebe61ec04..001d6138d 100644 --- a/apps/stage-tamagotchi/src/main/services/electron/window.ts +++ b/apps/stage-tamagotchi/src/main/services/electron/window.ts @@ -16,7 +16,7 @@ import { electronWindowSetAlwaysOnTop, } from '../../../shared/eventa' import { onAppBeforeQuit, onAppWindowAllClosed } from '../../libs/bootkit/lifecycle' -import { resizeWindowByDelta } from '../../windows/shared/window' +import { resizeWindowByDelta, setWindowAlwaysOnTop } from '../../windows/shared/window' export function createWindowService(params: { context: ReturnType['context'], window: BrowserWindow }) { function getWindowLifecycleState(reason: ElectronWindowLifecycleState['reason']): ElectronWindowLifecycleState { @@ -82,12 +82,7 @@ export function createWindowService(params: { context: ReturnType { if (params.window.webContents.id === options?.raw.ipcMainEvent.sender.id) { - if (flag) { - params.window.setAlwaysOnTop(true, 'screen-saver', 1) - } - else { - params.window.setAlwaysOnTop(false) - } + setWindowAlwaysOnTop(params.window, Boolean(flag)) } }) diff --git a/apps/stage-tamagotchi/src/main/windows/caption/index.ts b/apps/stage-tamagotchi/src/main/windows/caption/index.ts index 5e254ee74..c5ca6e718 100644 --- a/apps/stage-tamagotchi/src/main/windows/caption/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/caption/index.ts @@ -22,7 +22,7 @@ import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs import { createConfig } from '../../libs/electron/persistence' import { createReusableWindow } from '../../libs/electron/window-manager' import { mapForBreakpoints, resolutionBreakpoints, widthFrom } from '../shared/display' -import { protectPrivilegedWindowNavigation, setupBaseWindowElectronInvokes, transparentWindowConfig } from '../shared/window' +import { protectPrivilegedWindowNavigation, setupBaseWindowElectronInvokes, setWindowAlwaysOnTop, transparentWindowConfig } from '../shared/window' const captionConfigSchema = object({ isFollowing: boolean(), @@ -118,7 +118,7 @@ function createCaptionWindow(options?: BrowserWindowConstructorOptions) { // // https://github.com/electron/electron/issues/10078#issuecomment-3410164802 // https://stackoverflow.com/questions/39835282/set-browserwindow-always-on-top-even-other-app-is-in-fullscreen-electron-mac - type: 'panel', + type: isMacOS ? 'panel' : undefined, ...transparentWindowConfig(), ...options, }) @@ -129,12 +129,12 @@ function createCaptionWindow(options?: BrowserWindowConstructorOptions) { // // https://github.com/electron/electron/issues/10078#issuecomment-3410164802 // https://stackoverflow.com/questions/39835282/set-browserwindow-always-on-top-even-other-app-is-in-fullscreen-electron-mac - window.setAlwaysOnTop(true, 'screen-saver', 2) - window.setFullScreenable(false) window.setVisibleOnAllWorkspaces(true) if (isMacOS) { + window.setFullScreenable(false) window.setWindowButtonVisibility(false) } + setWindowAlwaysOnTop(window, true, 2) window.on('ready-to-show', () => window.show()) protectPrivilegedWindowNavigation(window) diff --git a/apps/stage-tamagotchi/src/main/windows/main/index.ts b/apps/stage-tamagotchi/src/main/windows/main/index.ts index 6e9b32590..391455dc6 100644 --- a/apps/stage-tamagotchi/src/main/windows/main/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/main/index.ts @@ -31,7 +31,7 @@ import { electronStartDraggingWindow } from '../../../shared/eventa' import { onAppBeforeQuit } from '../../libs/bootkit/lifecycle' import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs/electron/location' import { createConfig } from '../../libs/electron/persistence' -import { protectPrivilegedWindowNavigation, transparentWindowConfig } from '../shared' +import { protectPrivilegedWindowNavigation, setWindowAlwaysOnTop, transparentWindowConfig } from '../shared' import { setupMainWindowElectronInvokes } from './rpc/index.electron' const appConfigSchema = object({ @@ -91,7 +91,7 @@ export async function setupMainWindow(params: { // // https://github.com/electron/electron/issues/10078#issuecomment-3410164802 // https://stackoverflow.com/questions/39835282/set-browserwindow-always-on-top-even-other-app-is-in-fullscreen-electron-mac - type: 'panel', + type: isMacOS ? 'panel' : undefined, ...transparentWindowConfig(), }) @@ -161,12 +161,12 @@ export async function setupMainWindow(params: { // // https://github.com/electron/electron/issues/10078#issuecomment-3410164802 // https://stackoverflow.com/questions/39835282/set-browserwindow-always-on-top-even-other-app-is-in-fullscreen-electron-mac - window.setAlwaysOnTop(true, 'screen-saver', 1) - window.setFullScreenable(false) window.setVisibleOnAllWorkspaces(true) if (isMacOS) { + window.setFullScreenable(false) window.setWindowButtonVisibility(false) } + setWindowAlwaysOnTop(window, true) window.on('ready-to-show', () => window!.show()) protectPrivilegedWindowNavigation(window) diff --git a/apps/stage-tamagotchi/src/main/windows/shared/display.test.ts b/apps/stage-tamagotchi/src/main/windows/shared/display.test.ts index 81b7e833d..8e0cad4aa 100644 --- a/apps/stage-tamagotchi/src/main/windows/shared/display.test.ts +++ b/apps/stage-tamagotchi/src/main/windows/shared/display.test.ts @@ -41,6 +41,22 @@ describe('mapForBreakpoints', () => { const val2 = mapForBreakpoints(2000, { 'sm': 100, 'md': 200, '2xl': 500 }) // expected to be lg expect(val2).toBe(500) }) + + it('should fall back to the breakpoint with the smallest minimum width when below all breakpoints', () => { + // ROOT CAUSE: + // + // When `basedOn` is below every supplied breakpoint (e.g. display height 500 + // with sm/md/lg sizes), no breakpoint matches and the previous fallback was + // `Object.values(sizes)[0]`, which depends on object key insertion order. + // Sorting the sizes map (e.g. sm/md/lg -> lg/md/sm by a lint rule) changed + // the fallback from the sm formula to the lg formula and moved the inlay on + // small displays. + // + // We fixed this by selecting the breakpoint with the smallest minimum width + // explicitly, so the result is stable regardless of key order. + expect(mapForBreakpoints(500, { sm: 100, md: 200, lg: 300 })).toBe(100) + expect(mapForBreakpoints(500, { lg: 300, md: 200, sm: 100 })).toBe(100) + }) }) describe('widthFrom', () => { diff --git a/apps/stage-tamagotchi/src/main/windows/shared/display.ts b/apps/stage-tamagotchi/src/main/windows/shared/display.ts index 85d1ef83c..09f6232c4 100644 --- a/apps/stage-tamagotchi/src/main/windows/shared/display.ts +++ b/apps/stage-tamagotchi/src/main/windows/shared/display.ts @@ -220,7 +220,7 @@ export function mapForBreakpoints< basedOn: number, sizes: { [key in keyof B]?: number } | number, options?: { breakpoints: B }, -) { +): number { if (typeof sizes === 'number') { return sizes } @@ -244,8 +244,16 @@ export function mapForBreakpoints< .sort((a, b) => b.min - a.min) // Sort descending by min width const fallback = sortedSizes.find(s => s.min <= basedOn) + if (fallback?.value != null) { + return fallback.value + } - return fallback?.value ?? Object.values(sizes)?.[0] ?? 0 + // `basedOn` is below every supplied breakpoint (e.g. height < 640 with sm/md/lg + // sizes): use the breakpoint with the smallest minimum width. Selecting by min + // instead of `Object.values(sizes)[0]` keeps the result stable when the sizes + // object keys are reordered (e.g. by lint sorting rules). + const smallest = sortedSizes[sortedSizes.length - 1] + return smallest?.value ?? 0 } /** diff --git a/apps/stage-tamagotchi/src/main/windows/shared/index.ts b/apps/stage-tamagotchi/src/main/windows/shared/index.ts index 16311714e..e4d0a0cd8 100644 --- a/apps/stage-tamagotchi/src/main/windows/shared/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/shared/index.ts @@ -1 +1 @@ -export { protectPrivilegedWindowNavigation, toggleWindowShow, transparentWindowConfig } from './window' +export { protectPrivilegedWindowNavigation, setWindowAlwaysOnTop, toggleWindowShow, transparentWindowConfig } from './window' diff --git a/apps/stage-tamagotchi/src/main/windows/shared/window.test.ts b/apps/stage-tamagotchi/src/main/windows/shared/window.test.ts new file mode 100644 index 000000000..8075209e6 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/windows/shared/window.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it, vi } from 'vitest' + +import { setWindowAlwaysOnTop } from './window' + +const mocks = vi.hoisted(() => ({ + isMacOS: false, + isWindows: false, +})) + +vi.mock('electron', () => ({ + shell: { + openExternal: vi.fn(), + }, + BrowserWindow: vi.fn(), +})) + +vi.mock('std-env', () => ({ + get isMacOS() { + return mocks.isMacOS + }, + get isWindows() { + return mocks.isWindows + }, +})) + +vi.mock('../../services/electron', () => ({ + createAppService: vi.fn(), + createPowerMonitorService: vi.fn(), + createScreenService: vi.fn(), + createSystemPreferencesService: vi.fn(), + createWindowService: vi.fn(), +})) + +describe('setWindowAlwaysOnTop', () => { + it('disables always-on-top when flag is false', () => { + const window = { + setAlwaysOnTop: vi.fn(), + } + + setWindowAlwaysOnTop(window, false) + + expect(window.setAlwaysOnTop).toHaveBeenCalledWith(false) + }) + + it('applies standard always-on-top on Linux', () => { + mocks.isMacOS = false + mocks.isWindows = false + + const window = { + setAlwaysOnTop: vi.fn(), + } + + setWindowAlwaysOnTop(window, true) + + expect(window.setAlwaysOnTop).toHaveBeenCalledWith(true) + }) + + it('applies screen-saver level and relative offset on macOS', () => { + mocks.isMacOS = true + mocks.isWindows = false + + const window = { + setAlwaysOnTop: vi.fn(), + } + + setWindowAlwaysOnTop(window, true, 1) + + expect(window.setAlwaysOnTop).toHaveBeenCalledWith(true, 'screen-saver', 1) + }) + + it('applies screen-saver level and relative offset on Windows', () => { + mocks.isMacOS = false + mocks.isWindows = true + + const window = { + setAlwaysOnTop: vi.fn(), + } + + setWindowAlwaysOnTop(window, true, 2) + + expect(window.setAlwaysOnTop).toHaveBeenCalledWith(true, 'screen-saver', 2) + }) +}) diff --git a/apps/stage-tamagotchi/src/main/windows/shared/window.ts b/apps/stage-tamagotchi/src/main/windows/shared/window.ts index cf8761d3f..6d14d204b 100644 --- a/apps/stage-tamagotchi/src/main/windows/shared/window.ts +++ b/apps/stage-tamagotchi/src/main/windows/shared/window.ts @@ -7,7 +7,7 @@ import type { ServerChannel } from '../../services/airi/channel-server' import { isRendererUnavailable } from '@proj-airi/electron-vueuse/main' import { shell } from 'electron' -import { isMacOS } from 'std-env' +import { isMacOS, isWindows } from 'std-env' import { createServerChannelService } from '../../services/airi/channel-server' import { createI18nService } from '../../services/airi/i18n' @@ -92,6 +92,30 @@ export function spotlightLikeWindowConfig(): BrowserWindowConstructorOptions { } } +/** + * Sets the window always-on-top level according to the host platform. + * + * macOS and Windows support the screen-saver level and relative level layering, + * while Linux (X11/Wayland) works reliably with standard always-on-top. + */ +export function setWindowAlwaysOnTop( + window: Pick, + flag: boolean, + relativeLevel = 1, +): void { + if (!flag) { + window.setAlwaysOnTop(false) + return + } + + if (isMacOS || isWindows) { + window.setAlwaysOnTop(true, 'screen-saver', relativeLevel) + return + } + + window.setAlwaysOnTop(true) +} + export function resizeWindowByDelta(params: { window: BrowserWindow deltaX: number diff --git a/apps/stage-tamagotchi/src/main/windows/widgets/index.ts b/apps/stage-tamagotchi/src/main/windows/widgets/index.ts index 0a3cceb8a..e648f1446 100644 --- a/apps/stage-tamagotchi/src/main/windows/widgets/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/widgets/index.ts @@ -27,7 +27,7 @@ import { normalizeWidgetWindowSize } from '../../../shared/utils/electron/window import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs/electron/location' import { createConfig } from '../../libs/electron/persistence' import { createReusableWindow } from '../../libs/electron/window-manager' -import { protectPrivilegedWindowNavigation, spotlightLikeWindowConfig, transparentWindowConfig } from '../shared/window' +import { protectPrivilegedWindowNavigation, setWindowAlwaysOnTop, spotlightLikeWindowConfig, transparentWindowConfig } from '../shared/window' import { createWidgetIframeRequestCoordinator } from './iframe-request-coordinator' import { setupWidgetsWindowInvokes } from './rpc/index.electron' @@ -231,7 +231,7 @@ function createWidgetsWindow() { sandbox: false, }, // Top-level overlay style like other overlay windows - type: 'panel', + type: isMacOS ? 'panel' : undefined, ...transparentWindowConfig(), ...spotlightLikeWindowConfig(), }) @@ -247,15 +247,6 @@ function createWidgetsWindow() { return window } -function applyAlwaysOnTop(window: BrowserWindow, enabled: boolean) { - if (enabled) { - window.setAlwaysOnTop(true, 'screen-saver', 1) - return - } - - window.setAlwaysOnTop(false) -} - interface WidgetRecord extends WidgetSnapshot { timer?: ReturnType } @@ -512,7 +503,7 @@ export function setupWidgetsWindowManager(params: { const window = await getWindowFromContext(context) pendingRoute = undefined applyWindowLayout(window, snapshot) - applyAlwaysOnTop(window, snapshot?.alwaysOnTop ?? false) + setWindowAlwaysOnTop(window, snapshot?.alwaysOnTop ?? false) if (currentRoute !== route) await loadWithRoute(window, route) window.show() @@ -623,7 +614,7 @@ export function setupWidgetsWindowManager(params: { const window = context?.window if (window && !window.isDestroyed()) { applyWindowLayout(window, nextSnapshot) - applyAlwaysOnTop(window, nextSnapshot.alwaysOnTop) + setWindowAlwaysOnTop(window, nextSnapshot.alwaysOnTop) } eventaContext?.emit(widgetsUpdateEvent, { diff --git a/eslint.config.ts b/eslint.config.ts index 2a425d05b..d48354c69 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -21,6 +21,8 @@ export default defineConfig({ 'apps/stage-tamagotchi/src/bindings/**', 'apps/stage-tamagotchi-electron/out/**', 'apps/stage-tamagotchi-electron/src/renderer/bindings/**', + '**/flatpak/**', + '**/flatpak-repo/**', 'apps/stage-pocket/ios/**', 'apps/stage-pocket/android/**', '**/drizzle/**', diff --git a/integrations/minecraft/src/skills/actions/world-interactions.ts b/integrations/minecraft/src/skills/actions/world-interactions.ts index 3cae32756..204b66932 100644 --- a/integrations/minecraft/src/skills/actions/world-interactions.ts +++ b/integrations/minecraft/src/skills/actions/world-interactions.ts @@ -98,6 +98,13 @@ export async function placeBlock( west: new Vec3(-1, 0, 0), } + // Fallback search priority when the requested face is unavailable. Kept as an + // explicit array because the first fallback decides which face the bot attaches + // to, which can change the placed block's orientation; relying on + // `Object.values(dirMap)` would silently change this priority if the dirMap + // keys are ever reordered (e.g. by sorting). + const fallbackDirs = [dirMap.top, dirMap.bottom, dirMap.north, dirMap.south, dirMap.east, dirMap.west] + const dirs: Vec3[] = [] if (placeOn === 'side') { dirs.push(dirMap.north, dirMap.south, dirMap.east, dirMap.west) @@ -111,7 +118,7 @@ export async function placeBlock( } // Add remaining directions - dirs.push(...Object.values(dirMap).filter(d => !dirs.includes(d))) + dirs.push(...fallbackDirs.filter(d => !dirs.includes(d))) let buildOffBlock: Block | null = null let faceVec: Vec3 | null = null diff --git a/integrations/minecraft/src/skills/blocks.ts b/integrations/minecraft/src/skills/blocks.ts index 80d336142..465ff55d8 100644 --- a/integrations/minecraft/src/skills/blocks.ts +++ b/integrations/minecraft/src/skills/blocks.ts @@ -287,6 +287,13 @@ function findPlacementSpot(mineflayer: Mineflayer, targetDest: Vec3, placeOn: Bl } function getPlacementDirections(placeOn: BlockFace, dirMap: Record): Vec3[] { + // Fallback search priority when the requested face is unavailable. Kept as an + // explicit array because the first fallback decides which face the bot attaches + // to, which can change the placed block's orientation; relying on + // `Object.values(dirMap)` would silently change this priority if the dirMap + // keys are ever reordered (e.g. by sorting). + const fallbackDirs = [dirMap.top, dirMap.bottom, dirMap.north, dirMap.south, dirMap.east, dirMap.west] + const directions: Vec3[] = [] if (placeOn === 'side') { directions.push(dirMap.north, dirMap.south, dirMap.east, dirMap.west) @@ -298,7 +305,7 @@ function getPlacementDirections(placeOn: BlockFace, dirMap: Record directions.push(dirMap.bottom) } - directions.push(...Object.values(dirMap).filter(d => !directions.includes(d))) + directions.push(...fallbackDirs.filter(d => !directions.includes(d))) return directions }