diff --git a/oAI/oAIApp.swift b/oAI/oAIApp.swift index c9ce2f5..e72e5af 100644 --- a/oAI/oAIApp.swift +++ b/oAI/oAIApp.swift @@ -53,6 +53,41 @@ final class AppDelegate: NSObject, NSApplicationDelegate { return shouldTerminate ? .terminateNow : .terminateCancel } + // macOS's built-in window-restoration (Resume) can place the main window at coordinates + // that no longer correspond to any connected display (e.g. it was last positioned on an + // external monitor that's now disconnected). The window is still technically "visible" + // (isVisible == true), so clicking the Dock icon or Cmd+Tab-ing to the app doesn't trigger + // any of the usual recovery paths — the process runs fine, there's just nothing on screen. + // Self-heal by re-centering any main-sized window that doesn't intersect a current screen, + // both whenever the app becomes active and when the Dock icon is clicked with the app + // already running. + func applicationDidBecomeActive(_ notification: Notification) { + AppDelegate.repositionOffscreenWindows() + } + + func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + AppDelegate.repositionOffscreenWindows() + return true + } + + /// Pure geometry check, kept separate from AppKit so it's directly testable. + nonisolated static func isFrameOffscreen(_ frame: CGRect, screenFrames: [CGRect]) -> Bool { + !screenFrames.contains { $0.intersects(frame) } + } + + /// Re-centers the main window if it's off-screen, or unconditionally when `force` is true + /// (used by the manual "Reset Window Position" menu command). + static func repositionOffscreenWindows(force: Bool = false) { + let screenFrames = NSScreen.screens.map(\.frame) + for window in NSApp.windows where window.isVisible && !(window is NSPanel) && window.frame.width > 200 && window.frame.height > 200 { + if force || isFrameOffscreen(window.frame, screenFrames: screenFrames) { + window.center() + } + window.makeKeyAndOrderFront(nil) + } + NSApp.activate(ignoringOtherApps: true) + } + // `chatViewModel` is wired from `ContentView.onAppear`, not from `oAIApp.init()` — reading // the `@State private var chatViewModel` there returns a throwaway instance distinct from // the one SwiftUI actually renders (confirmed via ObjectIdentifier logging: two different @@ -217,6 +252,16 @@ struct oAIApp: App { chatViewModel.onlineMode.toggle() } .keyboardShortcut("o", modifiers: [.command, .shift]) + + Divider() + + // Fallback for when the window ends up off-screen (stale macOS window-restore + // coordinates, e.g. after an external monitor is disconnected) — see + // AppDelegate.repositionOffscreenWindows. No keyboard shortcut assigned yet; + // add one once it's been live-verified against macOS's reserved bindings. + Button("Reset Window Position") { + AppDelegate.repositionOffscreenWindows(force: true) + } } // ── Help menu ───────────────────────────────────────────────── diff --git a/oAITests/WindowPositionTests.swift b/oAITests/WindowPositionTests.swift new file mode 100644 index 0000000..2172030 --- /dev/null +++ b/oAITests/WindowPositionTests.swift @@ -0,0 +1,49 @@ +// +// WindowPositionTests.swift +// oAITests +// +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +// Copyright (C) 2026 Rune Olsen + +import Testing +import Foundation +import CoreGraphics +@testable import Confab + +@Suite("Off-screen window detection") +struct WindowPositionTests { + + private let mainScreen = CGRect(x: 0, y: 0, width: 2560, height: 1664) + + @Test("Window inside the only connected screen is not off-screen") + func onscreenWindow() { + let frame = CGRect(x: 100, y: 100, width: 1024, height: 800) + #expect(AppDelegate.isFrameOffscreen(frame, screenFrames: [mainScreen]) == false) + } + + @Test("Window at coordinates from a now-disconnected external display is off-screen") + func offscreenWindow() { + // e.g. previously positioned on an external monitor to the right of the built-in display + let frame = CGRect(x: 3000, y: 200, width: 1024, height: 800) + #expect(AppDelegate.isFrameOffscreen(frame, screenFrames: [mainScreen]) == true) + } + + @Test("Window merely partially overlapping a screen counts as on-screen") + func partiallyOverlappingWindow() { + let frame = CGRect(x: 2500, y: 100, width: 1024, height: 800) + #expect(AppDelegate.isFrameOffscreen(frame, screenFrames: [mainScreen]) == false) + } + + @Test("Off-screen relative to one display but onscreen on a second connected display") + func multiMonitorOnscreen() { + let secondScreen = CGRect(x: 2560, y: 0, width: 1920, height: 1080) + let frame = CGRect(x: 3000, y: 200, width: 1024, height: 800) + #expect(AppDelegate.isFrameOffscreen(frame, screenFrames: [mainScreen, secondScreen]) == false) + } + + @Test("No connected screens means every window is off-screen") + func noScreens() { + let frame = CGRect(x: 100, y: 100, width: 1024, height: 800) + #expect(AppDelegate.isFrameOffscreen(frame, screenFrames: []) == true) + } +}