From 721a26f77345eda9f4a6dd9bccff21f0dd9a4ed4 Mon Sep 17 00:00:00 2001 From: sionic-khope Date: Mon, 20 Jul 2026 19:37:54 +0900 Subject: [PATCH] Present the workspace on the display the idle Miku occupies The workspace window is created once and reused, so reopening it kept the display it was first created on even after the Miku panel moved to another monitor (screen-parameter changes rebind the panel to the display under the pointer). Each presentation now moves the window to the panel's current screen; same-screen opens keep the user's manual frame, and in-flight full-screen transitions are left alone. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015peMZyaHEmxDzyzNyhYeR5 --- .../Companion/AppPresentation.swift | 21 ++++++++ .../Companion/MikuPanelCoordinator.swift | 15 ++++++ Tests/MikuCodeAppTests/MikuCodeAppTests.swift | 49 +++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/Sources/MikuCodeApp/Companion/AppPresentation.swift b/Sources/MikuCodeApp/Companion/AppPresentation.swift index b32995c..95e4292 100644 --- a/Sources/MikuCodeApp/Companion/AppPresentation.swift +++ b/Sources/MikuCodeApp/Companion/AppPresentation.swift @@ -235,6 +235,27 @@ enum DisplayFrameSelector { } } +/// Decides where the reused workspace window must be placed before each +/// presentation. The window is created once, but the idle Miku panel can move +/// to another display between opens (screen-parameter changes rebind it to the +/// display under the pointer) — without this, the workspace reopens on +/// whichever display it was first created on. +enum WorkspaceDisplayPlacement { + /// Returns the frame the window should adopt, or nil to keep its current + /// frame (same display — preserves the user's manual size/position — or + /// mid-full-screen, where reframing fights the system transition). + static func frameForPresentation( + windowScreenFrame: CGRect?, + targetScreenFrame: CGRect, + targetVisibleFrame: CGRect, + isFullScreen: Bool + ) -> CGRect? { + guard !isFullScreen else { return nil } + guard windowScreenFrame != targetScreenFrame else { return nil } + return targetVisibleFrame + } +} + enum PresentationLayout { static func metrics(in size: CGSize) -> PresentationLayoutMetrics { let width = max(1, size.width) diff --git a/Sources/MikuCodeApp/Companion/MikuPanelCoordinator.swift b/Sources/MikuCodeApp/Companion/MikuPanelCoordinator.swift index 424df9f..557b579 100644 --- a/Sources/MikuCodeApp/Companion/MikuPanelCoordinator.swift +++ b/Sources/MikuCodeApp/Companion/MikuPanelCoordinator.swift @@ -51,6 +51,7 @@ final class MikuPanelCoordinator: NSObject, NSWindowDelegate { guard presentation.requestOpen() else { return } refreshScreenFrameFromPanel() let window = ensureWorkspaceWindow() + moveWorkspaceWindowToPanelDisplay(window) workspacePresentedAt = ProcessInfo.processInfo.systemUptime panel.orderOut(nil) guard ordersFront else { return } @@ -58,6 +59,20 @@ final class MikuPanelCoordinator: NSObject, NSWindowDelegate { window.makeKeyAndOrderFront(nil) } + /// The workspace window is created once and reused across opens; present it + /// on the display the idle Miku currently occupies, not wherever it was + /// created. + private func moveWorkspaceWindowToPanelDisplay(_ window: WorkspaceWindow) { + guard let targetScreen = panel.screen ?? NSScreen.main else { return } + guard let frame = WorkspaceDisplayPlacement.frameForPresentation( + windowScreenFrame: window.screen?.frame, + targetScreenFrame: targetScreen.frame, + targetVisibleFrame: targetScreen.visibleFrame, + isFullScreen: window.styleMask.contains(.fullScreen) + ) else { return } + window.setFrame(frame, display: true) + } + /// Close requested by clicking the workspace Miku. The click that opened the /// workspace lands on the same screen region the Miku close target appears /// in, and stale/replayed events from that press can fire the close button diff --git a/Tests/MikuCodeAppTests/MikuCodeAppTests.swift b/Tests/MikuCodeAppTests/MikuCodeAppTests.swift index ed593c3..d55f9a1 100644 --- a/Tests/MikuCodeAppTests/MikuCodeAppTests.swift +++ b/Tests/MikuCodeAppTests/MikuCodeAppTests.swift @@ -133,6 +133,55 @@ final class PresentationLayoutTests: XCTestCase { ) } + func testWorkspacePresentsOnTheDisplayTheIdleMikuOccupies() { + let builtIn = CGRect(x: 0, y: 0, width: 1_728, height: 1_117) + let external = CGRect(x: 1_728, y: -152, width: 1_920, height: 1_080) + let externalVisible = external.insetBy(dx: 0, dy: 12) + + // Window created on the built-in display, Miku now on the external one: + // the presentation must move it to the external display's visible frame. + XCTAssertEqual( + WorkspaceDisplayPlacement.frameForPresentation( + windowScreenFrame: builtIn, + targetScreenFrame: external, + targetVisibleFrame: externalVisible, + isFullScreen: false + ), + externalVisible + ) + + // A window that has never been on screen also moves to Miku's display. + XCTAssertEqual( + WorkspaceDisplayPlacement.frameForPresentation( + windowScreenFrame: nil, + targetScreenFrame: external, + targetVisibleFrame: externalVisible, + isFullScreen: false + ), + externalVisible + ) + + // Same display: keep the user's manually adjusted frame. + XCTAssertNil( + WorkspaceDisplayPlacement.frameForPresentation( + windowScreenFrame: external, + targetScreenFrame: external, + targetVisibleFrame: externalVisible, + isFullScreen: false + ) + ) + + // Never fight an in-flight full-screen transition. + XCTAssertNil( + WorkspaceDisplayPlacement.frameForPresentation( + windowScreenFrame: builtIn, + targetScreenFrame: external, + targetVisibleFrame: externalVisible, + isFullScreen: true + ) + ) + } + func testAdaptiveMetricsKeepTerminalLargeAndMikuFarRight() { let displaySizes = [ CGSize(width: 1_024, height: 768),