From abd28bffc6b77f3bba4d220e160137f224f6ffe8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 03:58:46 +0000 Subject: [PATCH 1/2] Initial plan From 2d9d7cd7943f582df9f0254c8011762d0e672a31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 04:07:32 +0000 Subject: [PATCH 2/2] fix: use popup position hint to identify display in multi-monitor setup In a multi-display environment, getScreenSize() was calling chrome.windows.getCurrent() from the service worker context to determine which display to use for bounds-checking. This could return a window on the primary/main display rather than the display where the user was actually working, causing adjustWindowPosition() to reposition the popup to the main display. Fix: add an optional `hint` parameter to getScreenSize(). When a hint (top, left) is provided, use those coordinates directly to find the correct display. Callers openPopupWindow() and openPopupWindowMultiple() now pass the popup target coordinates as the hint, so the display where the user is working is always correctly identified. Add screen.test.ts with 6 tests covering single/multi-display scenarios. Agent-Logs-Url: https://github.com/ujiro99/selection-command/sessions/c383e197-1956-435f-902e-69f98e6282bf Co-authored-by: ujiro99 <677231+ujiro99@users.noreply.github.com> --- packages/extension/src/services/chrome.ts | 4 +- .../extension/src/services/screen.test.ts | 160 ++++++++++++++++++ packages/extension/src/services/screen.ts | 35 ++-- 3 files changed, 183 insertions(+), 16 deletions(-) create mode 100644 packages/extension/src/services/screen.test.ts diff --git a/packages/extension/src/services/chrome.ts b/packages/extension/src/services/chrome.ts index e2d597d0..84b55401 100644 --- a/packages/extension/src/services/chrome.ts +++ b/packages/extension/src/services/chrome.ts @@ -446,7 +446,7 @@ export const openPopupWindow = async ( current = { id: undefined, incognito: false } as chrome.windows.Window } - const screenSize = await getScreenSize() + const screenSize = await getScreenSize({ top, left }) const type = param.type ?? POPUP_TYPE.POPUP const isFullscreen = param.windowState === WINDOW_STATE.FULLSCREEN @@ -553,7 +553,7 @@ export const openPopupWindowMultiple = async ( } const type = param.type ?? POPUP_TYPE.POPUP - const screenSize = await getScreenSize() + const screenSize = await getScreenSize({ top, left }) const windows = await Promise.all( param.urls diff --git a/packages/extension/src/services/screen.test.ts b/packages/extension/src/services/screen.test.ts new file mode 100644 index 00000000..c0c671ac --- /dev/null +++ b/packages/extension/src/services/screen.test.ts @@ -0,0 +1,160 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import { getScreenSize } from "./screen" + +// Mock isServiceWorker to control context +vi.mock("@/lib/utils", () => ({ + isServiceWorker: vi.fn(), +})) + +import { isServiceWorker } from "@/lib/utils" + +// Minimal display info structure for testing +const makeDisplay = ( + left: number, + top: number, + width: number, + height: number, + isPrimary = false, +): chrome.system.display.DisplayUnitInfo => + ({ + id: `display-${left}`, + name: `Display ${left}`, + isPrimary, + isEnabled: true, + bounds: { left, top, width, height }, + workArea: { left, top, width, height }, + overscan: { left: 0, top: 0, right: 0, bottom: 0 }, + rotation: 0, + dpiX: 96, + dpiY: 96, + mirroringSourceId: "", + mirroringDestinationIds: [], + isUnified: false, + activeState: "active", + displayZoomFactor: 1, + }) as unknown as chrome.system.display.DisplayUnitInfo + +describe("getScreenSize", () => { + const primaryDisplay = makeDisplay(0, 0, 1920, 1080, true) + const secondaryDisplay = makeDisplay(1920, 0, 2560, 1440, false) + + let mockGetInfo: ReturnType + let mockGetCurrent: ReturnType + + beforeEach(() => { + vi.clearAllMocks() + + mockGetInfo = vi.fn().mockResolvedValue([primaryDisplay, secondaryDisplay]) + mockGetCurrent = vi + .fn() + .mockResolvedValue({ left: 100, top: 100 } as chrome.windows.Window) + + vi.stubGlobal("chrome", { + system: { + display: { + getInfo: mockGetInfo, + }, + }, + windows: { + getCurrent: mockGetCurrent, + }, + }) + }) + + describe("In service worker context", () => { + beforeEach(() => { + vi.mocked(isServiceWorker).mockReturnValue(true) + }) + + it("GSS-01: ヒントなし - getCurrent() の結果からディスプレイを特定する", async () => { + // getCurrent returns position on primary display + mockGetCurrent.mockResolvedValue({ + left: 100, + top: 100, + } as chrome.windows.Window) + + const result = await getScreenSize() + + expect(result.left).toBe(0) + expect(result.top).toBe(0) + expect(result.width).toBe(1920) + expect(result.height).toBe(1080) + }) + + it("GSS-02: ヒントあり(プライマリディスプレイ上の座標)- プライマリディスプレイを返す", async () => { + const result = await getScreenSize({ top: 200, left: 300 }) + + // Should identify primary display (0,0,1920,1080) + expect(result.left).toBe(0) + expect(result.top).toBe(0) + expect(result.width).toBe(1920) + expect(result.height).toBe(1080) + // getCurrent should NOT be called when hint is provided + expect(mockGetCurrent).not.toHaveBeenCalled() + }) + + it("GSS-03: ヒントあり(セカンダリディスプレイ上の座標)- セカンダリディスプレイを返す", async () => { + // Position on secondary display (left=1920, right=4480) + const result = await getScreenSize({ top: 100, left: 2000 }) + + // Should identify secondary display (1920,0,2560,1440) + expect(result.left).toBe(1920) + expect(result.top).toBe(0) + expect(result.width).toBe(2560) + expect(result.height).toBe(1440) + // getCurrent should NOT be called when hint is provided + expect(mockGetCurrent).not.toHaveBeenCalled() + }) + + it("GSS-04: マルチディスプレイ環境でヒントがプライマリにない場合もセカンダリを正しく返す", async () => { + // Simulate: getCurrent returns primary window, but popup hint is on secondary display + mockGetCurrent.mockResolvedValue({ + left: 100, // on primary display + top: 100, + } as chrome.windows.Window) + + const result = await getScreenSize({ top: 500, left: 3000 }) + + // Should correctly identify secondary display, not primary + expect(result.left).toBe(1920) + expect(result.top).toBe(0) + expect(result.width).toBe(2560) + expect(result.height).toBe(1440) + }) + + it("GSS-05: ヒントがいずれのディスプレイにも含まれない場合、プライマリを返す", async () => { + // Position doesn't match any display + const result = await getScreenSize({ top: -100, left: -200 }) + + // Should fall back to primary display + expect(result.left).toBe(0) + expect(result.top).toBe(0) + expect(result.width).toBe(1920) + expect(result.height).toBe(1080) + }) + }) + + describe("In content script context (non-service-worker)", () => { + beforeEach(() => { + vi.mocked(isServiceWorker).mockReturnValue(false) + + // Mock window.screen + vi.stubGlobal("window", { + screen: { + width: 1920, + height: 1080, + availLeft: 0, + availTop: 0, + }, + }) + }) + + it("GSS-06: window.screen の値を返す(ヒントは無視)", async () => { + const result = await getScreenSize({ top: 500, left: 3000 }) + + expect(result.width).toBe(1920) + expect(result.height).toBe(1080) + }) + }) +}) + diff --git a/packages/extension/src/services/screen.ts b/packages/extension/src/services/screen.ts index 63f1bbcd..cc864239 100644 --- a/packages/extension/src/services/screen.ts +++ b/packages/extension/src/services/screen.ts @@ -37,29 +37,36 @@ export async function getWindowPosition(): Promise { } } -export async function getScreenSize(): Promise { +export async function getScreenSize(hint?: { + top: number + left: number +}): Promise { if (isServiceWorker()) { try { // For background_script.ts - const [displays, currentWindow] = await Promise.all([ - chrome.system.display.getInfo(), - chrome.windows.getCurrent(), - ]) + const displays = await chrome.system.display.getInfo() - let targetDisplay - const currentWindowLeft = currentWindow.left - const currentWindowTop = currentWindow.top + let hintTop = hint?.top + let hintLeft = hint?.left - if (currentWindowLeft != null && currentWindowTop != null) { - // Find the monitor that contains the active window's left position + if (hintTop == null || hintLeft == null) { + // Fall back to current window position if no hint provided + const currentWindow = await chrome.windows.getCurrent() + hintLeft = currentWindow.left ?? undefined + hintTop = currentWindow.top ?? undefined + } + + let targetDisplay + if (hintLeft != null && hintTop != null) { + // Find the monitor that contains the given position targetDisplay = displays.find((d) => { const a = d.workArea return ( - currentWindowLeft >= a.left && - currentWindowLeft < a.left + a.width && - currentWindowTop >= a.top && - currentWindowTop < a.top + a.height + hintLeft >= a.left && + hintLeft < a.left + a.width && + hintTop >= a.top && + hintTop < a.top + a.height ) }) ?? displays.find((d) => d.isPrimary) ??