From 854b3fa69a33b8cebbb69b55a509aa00831b278e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:01:26 +0000 Subject: [PATCH 01/14] Initial plan From 977ca90f896615f16d8f72245055c7dc16260ff9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:05:27 +0000 Subject: [PATCH 02/14] Add popupAutoCloseDelay setting with UI and types Co-authored-by: ujiro99 <677231+ujiro99@users.noreply.github.com> --- .../public/_locales/en/messages.json | 9 ++++ .../public/_locales/ja/messages.json | 9 ++++ packages/extension/src/background_script.ts | 42 +++++++++++++++++-- .../src/components/option/SettingForm.tsx | 26 ++++++++++++ .../src/services/option/defaultSettings.ts | 1 + packages/extension/src/types/index.ts | 1 + 6 files changed, 84 insertions(+), 4 deletions(-) diff --git a/packages/extension/public/_locales/en/messages.json b/packages/extension/public/_locales/en/messages.json index 077388d3..c517e6df 100644 --- a/packages/extension/public/_locales/en/messages.json +++ b/packages/extension/public/_locales/en/messages.json @@ -203,6 +203,15 @@ "Option_popupAnimation": { "message": "Menu Display Animation" }, + "Option_popupAutoCloseDelay": { + "message": "Popup Auto-Close Delay" + }, + "Option_popupAutoCloseDelay_desc": { + "message": "Set the delay time before the popup automatically closes after losing focus. Set to 0 or leave empty for immediate close." + }, + "Option_popupAutoCloseDelay_placeholder": { + "message": "0 (close immediately)" + }, "Option_inherit": { "message": "Inherit" }, diff --git a/packages/extension/public/_locales/ja/messages.json b/packages/extension/public/_locales/ja/messages.json index 5d956f30..71203b58 100644 --- a/packages/extension/public/_locales/ja/messages.json +++ b/packages/extension/public/_locales/ja/messages.json @@ -203,6 +203,15 @@ "Option_popupAnimation": { "message": "メニュー表示アニメーション" }, + "Option_popupAutoCloseDelay": { + "message": "ポップアップ自動クローズまでの遅延時間" + }, + "Option_popupAutoCloseDelay_desc": { + "message": "フォーカスが外れてからポップアップが自動的に閉じるまでの時間を設定します。0または未設定の場合は即座に閉じます。" + }, + "Option_popupAutoCloseDelay_placeholder": { + "message": "0 (即座に閉じる)" + }, "Option_inherit": { "message": "継承" }, diff --git a/packages/extension/src/background_script.ts b/packages/extension/src/background_script.ts index 98f80b5f..c3a481b4 100644 --- a/packages/extension/src/background_script.ts +++ b/packages/extension/src/background_script.ts @@ -33,6 +33,9 @@ BgData.init() type Sender = chrome.runtime.MessageSender +// Popup auto-close delay timer +let popupAutoCloseTimer: number | null = null + export type addPageRuleProps = { url: string } @@ -364,10 +367,41 @@ chrome.windows.onFocusChanged.addListener(async (windowId: number) => { // Get windows to close based on focus change const windowsToClose = await WindowStackManager.getWindowsToClose(windowId) - // Execute close for all windows that need to be closed - for (const window of windowsToClose) { - await closeWindow(window.id, "onFocusChanged") - await WindowStackManager.removeWindow(window.id) + // If there are no windows to close, cancel any pending auto-close timer + if (windowsToClose.length === 0) { + if (popupAutoCloseTimer !== null) { + clearTimeout(popupAutoCloseTimer) + popupAutoCloseTimer = null + } + return + } + + // Cancel any existing timer + if (popupAutoCloseTimer !== null) { + clearTimeout(popupAutoCloseTimer) + popupAutoCloseTimer = null + } + + // Get the auto-close delay setting + const settings = await Settings.get() + const autoCloseDelay = settings.popupAutoCloseDelay + + // Define the close function + const closeWindows = async () => { + for (const window of windowsToClose) { + await closeWindow(window.id, "onFocusChanged") + await WindowStackManager.removeWindow(window.id) + } + popupAutoCloseTimer = null + } + + // Execute close based on delay setting + if (autoCloseDelay != null && autoCloseDelay > 0) { + // Delayed close: Set timeout + popupAutoCloseTimer = setTimeout(closeWindows, autoCloseDelay) as unknown as number + } else { + // Immediate close: No delay configured + await closeWindows() } }) diff --git a/packages/extension/src/components/option/SettingForm.tsx b/packages/extension/src/components/option/SettingForm.tsx index 81a86adc..542232bc 100644 --- a/packages/extension/src/components/option/SettingForm.tsx +++ b/packages/extension/src/components/option/SettingForm.tsx @@ -79,6 +79,11 @@ const formSchema = z }) .strict(), popupPlacement: popupPlacementSchema, + popupAutoCloseDelay: z + .number({ message: t("zod_number") }) + .min(0, { message: t("zod_number_min", ["0"]) }) + .max(10000, { message: t("zod_number_max", ["10000"]) }) + .optional(), style: z.nativeEnum(STYLE), commands: z.array(commandSchema).min(1), folders: z.array(folderSchema), @@ -129,6 +134,7 @@ export function SettingForm({ className }: { className?: string }) { defaultValues: { startupMethod: emptySettings.startupMethod, popupPlacement: emptySettings.popupPlacement, + popupAutoCloseDelay: emptySettings.popupAutoCloseDelay, style: emptySettings.style, commands: [], // Empty array to avoid type conflicts folders: emptySettings.folders, @@ -464,6 +470,26 @@ export function SettingForm({ className }: { className?: string }) { defaultValues={getAnimationDefaultValues()} /> )} + + {startupMethod !== STARTUP_METHOD.CONTEXT_MENU && ( + + )}
diff --git a/packages/extension/src/services/option/defaultSettings.ts b/packages/extension/src/services/option/defaultSettings.ts index de0a40f1..934a792f 100644 --- a/packages/extension/src/services/option/defaultSettings.ts +++ b/packages/extension/src/services/option/defaultSettings.ts @@ -36,6 +36,7 @@ export const emptySettings: SettingsType = { alignOffset: 0, sideOffset: 0, }, + popupAutoCloseDelay: undefined, linkCommand: { enabled: LINK_COMMAND_ENABLED.ENABLE, openMode: DRAG_OPEN_MODE.PREVIEW_POPUP, diff --git a/packages/extension/src/types/index.ts b/packages/extension/src/types/index.ts index 1e53b8de..b29aeb0d 100644 --- a/packages/extension/src/types/index.ts +++ b/packages/extension/src/types/index.ts @@ -165,6 +165,7 @@ export type UserSettings = { settingVersion: Version startupMethod: StartupMethod popupPlacement: PopupPlacement + popupAutoCloseDelay?: number commands: Array linkCommand: LinkCommandSettings folders: Array From a89e97f0c47b3f5ee9acca8e24d395bfebf62e10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:06:57 +0000 Subject: [PATCH 03/14] Add tests for popup auto-close delay feature Co-authored-by: ujiro99 <677231+ujiro99@users.noreply.github.com> --- .../extension/src/background_script.test.ts | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) diff --git a/packages/extension/src/background_script.test.ts b/packages/extension/src/background_script.test.ts index 5efa0473..328a7e40 100644 --- a/packages/extension/src/background_script.test.ts +++ b/packages/extension/src/background_script.test.ts @@ -378,3 +378,239 @@ describe("Background Script Migration", () => { ) }) }) + +describe("Popup Auto-Close Delay", () => { + beforeEach(() => { + vi.clearAllMocks() + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it("PAC-01: should close popups immediately when delay is not set", async () => { + // Mock WindowStackManager + const mockGetWindowsToClose = vi.fn().mockResolvedValue([ + { id: 100, commandId: "test", srcWindowId: 1 }, + ]) + const mockRemoveWindow = vi.fn().mockResolvedValue(undefined) + + vi.doMock("@/services/windowStackManager", () => ({ + WindowStackManager: { + getWindowsToClose: mockGetWindowsToClose, + removeWindow: mockRemoveWindow, + }, + })) + + // Mock closeWindow + const mockCloseWindow = vi.fn().mockResolvedValue(undefined) + vi.doMock("@/services/chrome", () => ({ + closeWindow: mockCloseWindow, + windowExists: vi.fn(), + })) + + // Mock Settings to return no delay + mockSettings.get.mockResolvedValue({ + popupAutoCloseDelay: undefined, + commands: [], + folders: [], + pageRules: [], + stars: [], + shortcuts: { shortcuts: [] }, + commandExecutionCount: 0, + hasShownReviewRequest: false, + } as any) + + // Mock Storage + const mockStorageSet = vi.fn().mockResolvedValue(undefined) + vi.doMock("@/services/storage", () => ({ + Storage: { + set: mockStorageSet, + }, + SESSION_STORAGE_KEY: { + SELECTION_TEXT: "selectionText", + }, + })) + + // Mock updateActiveScreenId + vi.doMock("@/services/screen", () => ({ + updateActiveScreenId: vi.fn().mockResolvedValue(undefined), + })) + + // Clear module cache and re-import + vi.resetModules() + await import("./background_script") + + // Get the registered listener + const listenerCalls = (chrome.windows.onFocusChanged.addListener as any) + .mock.calls + expect(listenerCalls.length).toBeGreaterThan(0) + const focusChangedListener = listenerCalls[listenerCalls.length - 1][0] + + // Trigger focus change + await focusChangedListener(200) + + // Wait for async operations + await vi.runAllTimersAsync() + + // Verify window was closed immediately (no setTimeout) + expect(mockCloseWindow).toHaveBeenCalledWith(100, "onFocusChanged") + expect(mockRemoveWindow).toHaveBeenCalledWith(100) + }) + + it("PAC-02: should delay popup close when delay is set", async () => { + const delay = 1000 // 1 second + + // Mock WindowStackManager + const mockGetWindowsToClose = vi.fn().mockResolvedValue([ + { id: 100, commandId: "test", srcWindowId: 1 }, + ]) + const mockRemoveWindow = vi.fn().mockResolvedValue(undefined) + + vi.doMock("@/services/windowStackManager", () => ({ + WindowStackManager: { + getWindowsToClose: mockGetWindowsToClose, + removeWindow: mockRemoveWindow, + }, + })) + + // Mock closeWindow + const mockCloseWindow = vi.fn().mockResolvedValue(undefined) + vi.doMock("@/services/chrome", () => ({ + closeWindow: mockCloseWindow, + windowExists: vi.fn(), + })) + + // Mock Settings to return delay + mockSettings.get.mockResolvedValue({ + popupAutoCloseDelay: delay, + commands: [], + folders: [], + pageRules: [], + stars: [], + shortcuts: { shortcuts: [] }, + commandExecutionCount: 0, + hasShownReviewRequest: false, + } as any) + + // Mock Storage + const mockStorageSet = vi.fn().mockResolvedValue(undefined) + vi.doMock("@/services/storage", () => ({ + Storage: { + set: mockStorageSet, + }, + SESSION_STORAGE_KEY: { + SELECTION_TEXT: "selectionText", + }, + })) + + // Mock updateActiveScreenId + vi.doMock("@/services/screen", () => ({ + updateActiveScreenId: vi.fn().mockResolvedValue(undefined), + })) + + // Clear module cache and re-import + vi.resetModules() + await import("./background_script") + + // Get the registered listener + const listenerCalls = (chrome.windows.onFocusChanged.addListener as any) + .mock.calls + const focusChangedListener = listenerCalls[listenerCalls.length - 1][0] + + // Trigger focus change + await focusChangedListener(200) + + // Window should not be closed immediately + expect(mockCloseWindow).not.toHaveBeenCalled() + + // Advance timers by the delay amount + await vi.advanceTimersByTimeAsync(delay) + + // Now window should be closed + expect(mockCloseWindow).toHaveBeenCalledWith(100, "onFocusChanged") + expect(mockRemoveWindow).toHaveBeenCalledWith(100) + }) + + it("PAC-03: should cancel timeout when focus returns before delay", async () => { + const delay = 1000 // 1 second + + // Mock WindowStackManager - first returns windows to close, then empty array + const mockGetWindowsToClose = vi + .fn() + .mockResolvedValueOnce([{ id: 100, commandId: "test", srcWindowId: 1 }]) + .mockResolvedValueOnce([]) // No windows to close when focus returns + const mockRemoveWindow = vi.fn().mockResolvedValue(undefined) + + vi.doMock("@/services/windowStackManager", () => ({ + WindowStackManager: { + getWindowsToClose: mockGetWindowsToClose, + removeWindow: mockRemoveWindow, + }, + })) + + // Mock closeWindow + const mockCloseWindow = vi.fn().mockResolvedValue(undefined) + vi.doMock("@/services/chrome", () => ({ + closeWindow: mockCloseWindow, + windowExists: vi.fn(), + })) + + // Mock Settings to return delay + mockSettings.get.mockResolvedValue({ + popupAutoCloseDelay: delay, + commands: [], + folders: [], + pageRules: [], + stars: [], + shortcuts: { shortcuts: [] }, + commandExecutionCount: 0, + hasShownReviewRequest: false, + } as any) + + // Mock Storage + const mockStorageSet = vi.fn().mockResolvedValue(undefined) + vi.doMock("@/services/storage", () => ({ + Storage: { + set: mockStorageSet, + }, + SESSION_STORAGE_KEY: { + SELECTION_TEXT: "selectionText", + }, + })) + + // Mock updateActiveScreenId + vi.doMock("@/services/screen", () => ({ + updateActiveScreenId: vi.fn().mockResolvedValue(undefined), + })) + + // Clear module cache and re-import + vi.resetModules() + await import("./background_script") + + // Get the registered listener + const listenerCalls = (chrome.windows.onFocusChanged.addListener as any) + .mock.calls + const focusChangedListener = listenerCalls[listenerCalls.length - 1][0] + + // Trigger focus change (popup loses focus) + await focusChangedListener(200) + + // Window should not be closed yet + expect(mockCloseWindow).not.toHaveBeenCalled() + + // Advance timers only halfway + await vi.advanceTimersByTimeAsync(delay / 2) + + // Focus returns to popup (no windows to close) + await focusChangedListener(100) + + // Advance remaining time + await vi.advanceTimersByTimeAsync(delay / 2 + 100) + + // Window should NOT be closed because timeout was cancelled + expect(mockCloseWindow).not.toHaveBeenCalled() + expect(mockRemoveWindow).not.toHaveBeenCalled() + }) +}) From 384a8b2b0232acf8e43ea7b56002545e0d113f83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:08:58 +0000 Subject: [PATCH 04/14] Fix timer type to use NodeJS.Timeout instead of number Co-authored-by: ujiro99 <677231+ujiro99@users.noreply.github.com> --- packages/extension/src/background_script.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/extension/src/background_script.ts b/packages/extension/src/background_script.ts index c3a481b4..7e8a7351 100644 --- a/packages/extension/src/background_script.ts +++ b/packages/extension/src/background_script.ts @@ -34,7 +34,7 @@ BgData.init() type Sender = chrome.runtime.MessageSender // Popup auto-close delay timer -let popupAutoCloseTimer: number | null = null +let popupAutoCloseTimer: NodeJS.Timeout | null = null export type addPageRuleProps = { url: string @@ -398,7 +398,7 @@ chrome.windows.onFocusChanged.addListener(async (windowId: number) => { // Execute close based on delay setting if (autoCloseDelay != null && autoCloseDelay > 0) { // Delayed close: Set timeout - popupAutoCloseTimer = setTimeout(closeWindows, autoCloseDelay) as unknown as number + popupAutoCloseTimer = setTimeout(closeWindows, autoCloseDelay) } else { // Immediate close: No delay configured await closeWindows() From ecf1270fcd089624747ad98bdf4b9ea6994e5ae0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:09:57 +0000 Subject: [PATCH 05/14] Improve validation and comparison logic per code review Co-authored-by: ujiro99 <677231+ujiro99@users.noreply.github.com> --- packages/extension/src/background_script.ts | 2 +- packages/extension/src/components/option/SettingForm.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/extension/src/background_script.ts b/packages/extension/src/background_script.ts index 7e8a7351..6dd5ebf5 100644 --- a/packages/extension/src/background_script.ts +++ b/packages/extension/src/background_script.ts @@ -396,7 +396,7 @@ chrome.windows.onFocusChanged.addListener(async (windowId: number) => { } // Execute close based on delay setting - if (autoCloseDelay != null && autoCloseDelay > 0) { + if (autoCloseDelay !== undefined && autoCloseDelay > 0) { // Delayed close: Set timeout popupAutoCloseTimer = setTimeout(closeWindows, autoCloseDelay) } else { diff --git a/packages/extension/src/components/option/SettingForm.tsx b/packages/extension/src/components/option/SettingForm.tsx index 542232bc..d2766f69 100644 --- a/packages/extension/src/components/option/SettingForm.tsx +++ b/packages/extension/src/components/option/SettingForm.tsx @@ -83,6 +83,7 @@ const formSchema = z .number({ message: t("zod_number") }) .min(0, { message: t("zod_number_min", ["0"]) }) .max(10000, { message: t("zod_number_max", ["10000"]) }) + .nullable() .optional(), style: z.nativeEnum(STYLE), commands: z.array(commandSchema).min(1), From 8b6e4d32c7adc0a957fcbe9c0f3f845278db5dbc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:11:50 +0000 Subject: [PATCH 06/14] Fix input handling to convert empty string to undefined Co-authored-by: ujiro99 <677231+ujiro99@users.noreply.github.com> --- packages/extension/src/components/option/SettingForm.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/extension/src/components/option/SettingForm.tsx b/packages/extension/src/components/option/SettingForm.tsx index d2766f69..701b90d8 100644 --- a/packages/extension/src/components/option/SettingForm.tsx +++ b/packages/extension/src/components/option/SettingForm.tsx @@ -83,7 +83,6 @@ const formSchema = z .number({ message: t("zod_number") }) .min(0, { message: t("zod_number_min", ["0"]) }) .max(10000, { message: t("zod_number_max", ["10000"]) }) - .nullable() .optional(), style: z.nativeEnum(STYLE), commands: z.array(commandSchema).min(1), @@ -486,7 +485,7 @@ export function SettingForm({ className }: { className?: string }) { step: 100, placeholder: t("popupAutoCloseDelay_placeholder"), ...register("popupAutoCloseDelay", { - valueAsNumber: true, + setValueAs: (v) => (v === "" || isNaN(v) ? undefined : Number(v)), }), }} /> From fc83432ec35ff9dea8da68ea3cd383d6564cbc93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:12:31 +0000 Subject: [PATCH 07/14] Fix setValueAs to properly handle empty string conversion Co-authored-by: ujiro99 <677231+ujiro99@users.noreply.github.com> --- packages/extension/src/components/option/SettingForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension/src/components/option/SettingForm.tsx b/packages/extension/src/components/option/SettingForm.tsx index 701b90d8..0c04a031 100644 --- a/packages/extension/src/components/option/SettingForm.tsx +++ b/packages/extension/src/components/option/SettingForm.tsx @@ -485,7 +485,7 @@ export function SettingForm({ className }: { className?: string }) { step: 100, placeholder: t("popupAutoCloseDelay_placeholder"), ...register("popupAutoCloseDelay", { - setValueAs: (v) => (v === "" || isNaN(v) ? undefined : Number(v)), + setValueAs: (v) => (v === "" ? undefined : Number(v)), }), }} /> From b3fb92066106943644df017307eb5e90aed8c5c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:31:37 +0000 Subject: [PATCH 08/14] Simplify timer clearing logic to avoid redundancy Co-authored-by: ujiro99 <677231+ujiro99@users.noreply.github.com> --- packages/extension/src/background_script.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/extension/src/background_script.ts b/packages/extension/src/background_script.ts index 6dd5ebf5..659a700f 100644 --- a/packages/extension/src/background_script.ts +++ b/packages/extension/src/background_script.ts @@ -364,24 +364,20 @@ chrome.windows.onFocusChanged.addListener(async (windowId: number) => { // Update active screen ID await updateActiveScreenId(windowId) + // Cancel any existing timer + if (popupAutoCloseTimer !== null) { + clearTimeout(popupAutoCloseTimer) + popupAutoCloseTimer = null + } + // Get windows to close based on focus change const windowsToClose = await WindowStackManager.getWindowsToClose(windowId) - // If there are no windows to close, cancel any pending auto-close timer + // If there are no windows to close, nothing to do if (windowsToClose.length === 0) { - if (popupAutoCloseTimer !== null) { - clearTimeout(popupAutoCloseTimer) - popupAutoCloseTimer = null - } return } - // Cancel any existing timer - if (popupAutoCloseTimer !== null) { - clearTimeout(popupAutoCloseTimer) - popupAutoCloseTimer = null - } - // Get the auto-close delay setting const settings = await Settings.get() const autoCloseDelay = settings.popupAutoCloseDelay From 98a0c99708023e0035f497503bf78e9238818feb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:02:53 +0000 Subject: [PATCH 09/14] Refactor popup auto-close into dedicated service module Co-authored-by: ujiro99 <677231+ujiro99@users.noreply.github.com> --- packages/extension/src/background_script.ts | 38 +---- .../src/services/popupAutoClose.test.ts | 144 ++++++++++++++++++ .../extension/src/services/popupAutoClose.ts | 59 +++++++ 3 files changed, 206 insertions(+), 35 deletions(-) create mode 100644 packages/extension/src/services/popupAutoClose.test.ts create mode 100644 packages/extension/src/services/popupAutoClose.ts diff --git a/packages/extension/src/background_script.ts b/packages/extension/src/background_script.ts index 184f9a80..2d463874 100644 --- a/packages/extension/src/background_script.ts +++ b/packages/extension/src/background_script.ts @@ -17,6 +17,7 @@ import { BgData } from "@/services/backgroundData" import { ContextMenu } from "@/services/contextMenus" import { closeWindow, windowExists } from "@/services/chrome" import { WindowStackManager } from "@/services/windowStackManager" +import { PopupAutoClose } from "@/services/popupAutoClose" import { isSearchCommand, isPageActionCommand } from "@/lib/utils" import { execute } from "@/action/background" import * as ActionHelper from "@/action/helper" @@ -33,9 +34,6 @@ BgData.init() type Sender = chrome.runtime.MessageSender -// Popup auto-close delay timer -let popupAutoCloseTimer: NodeJS.Timeout | null = null - export type addPageRuleProps = { url: string } @@ -379,41 +377,11 @@ chrome.windows.onFocusChanged.addListener(async (windowId: number) => { // Update active screen ID await updateActiveScreenId(windowId) - // Cancel any existing timer - if (popupAutoCloseTimer !== null) { - clearTimeout(popupAutoCloseTimer) - popupAutoCloseTimer = null - } - // Get windows to close based on focus change const windowsToClose = await WindowStackManager.getWindowsToClose(windowId) - // If there are no windows to close, nothing to do - if (windowsToClose.length === 0) { - return - } - - // Get the auto-close delay setting - const settings = await Settings.get() - const autoCloseDelay = settings.popupAutoCloseDelay - - // Define the close function - const closeWindows = async () => { - for (const window of windowsToClose) { - await closeWindow(window.id, "onFocusChanged") - await WindowStackManager.removeWindow(window.id) - } - popupAutoCloseTimer = null - } - - // Execute close based on delay setting - if (autoCloseDelay !== undefined && autoCloseDelay > 0) { - // Delayed close: Set timeout - popupAutoCloseTimer = setTimeout(closeWindows, autoCloseDelay) - } else { - // Immediate close: No delay configured - await closeWindows() - } + // Schedule popup windows to close with configured delay + await PopupAutoClose.scheduleClose(windowsToClose) }) chrome.windows.onRemoved.addListener((windowId: number) => { diff --git a/packages/extension/src/services/popupAutoClose.test.ts b/packages/extension/src/services/popupAutoClose.test.ts new file mode 100644 index 00000000..ffbcc382 --- /dev/null +++ b/packages/extension/src/services/popupAutoClose.test.ts @@ -0,0 +1,144 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { PopupAutoClose } from "./popupAutoClose" +import { Settings } from "@/services/settings/settings" +import { closeWindow } from "@/services/chrome" +import { WindowStackManager } from "@/services/windowStackManager" + +// Mock dependencies +vi.mock("@/services/settings/settings") +vi.mock("@/services/chrome") +vi.mock("@/services/windowStackManager") + +const mockSettings = vi.mocked(Settings) +const mockCloseWindow = vi.mocked(closeWindow) +const mockWindowStackManager = vi.mocked(WindowStackManager) + +describe("PopupAutoClose", () => { + beforeEach(() => { + vi.clearAllMocks() + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + describe("cancelTimer", () => { + it("should cancel pending timer", () => { + // No error should be thrown even if no timer is set + expect(() => PopupAutoClose.cancelTimer()).not.toThrow() + }) + }) + + describe("scheduleClose", () => { + it("should close windows immediately when delay is undefined", async () => { + const windows = [{ id: 100, commandId: "test", srcWindowId: 1 }] + + mockSettings.get.mockResolvedValue({ + popupAutoCloseDelay: undefined, + } as any) + + await PopupAutoClose.scheduleClose(windows) + + // Should close immediately without waiting for timer + expect(mockCloseWindow).toHaveBeenCalledWith(100, "onFocusChanged") + expect(mockWindowStackManager.removeWindow).toHaveBeenCalledWith(100) + }) + + it("should close windows immediately when delay is 0", async () => { + const windows = [{ id: 100, commandId: "test", srcWindowId: 1 }] + + mockSettings.get.mockResolvedValue({ + popupAutoCloseDelay: 0, + } as any) + + await PopupAutoClose.scheduleClose(windows) + + // Should close immediately without waiting for timer + expect(mockCloseWindow).toHaveBeenCalledWith(100, "onFocusChanged") + expect(mockWindowStackManager.removeWindow).toHaveBeenCalledWith(100) + }) + + it("should delay close when delay is set", async () => { + const delay = 1000 + const windows = [{ id: 100, commandId: "test", srcWindowId: 1 }] + + mockSettings.get.mockResolvedValue({ + popupAutoCloseDelay: delay, + } as any) + + await PopupAutoClose.scheduleClose(windows) + + // Should not close immediately + expect(mockCloseWindow).not.toHaveBeenCalled() + + // Advance timers + await vi.advanceTimersByTimeAsync(delay) + + // Should close after delay + expect(mockCloseWindow).toHaveBeenCalledWith(100, "onFocusChanged") + expect(mockWindowStackManager.removeWindow).toHaveBeenCalledWith(100) + }) + + it("should close multiple windows", async () => { + const windows = [ + { id: 100, commandId: "test1", srcWindowId: 1 }, + { id: 101, commandId: "test2", srcWindowId: 1 }, + ] + + mockSettings.get.mockResolvedValue({ + popupAutoCloseDelay: undefined, + } as any) + + await PopupAutoClose.scheduleClose(windows) + + // Should close all windows + expect(mockCloseWindow).toHaveBeenCalledTimes(2) + expect(mockCloseWindow).toHaveBeenCalledWith(100, "onFocusChanged") + expect(mockCloseWindow).toHaveBeenCalledWith(101, "onFocusChanged") + expect(mockWindowStackManager.removeWindow).toHaveBeenCalledTimes(2) + }) + + it("should do nothing when no windows to close", async () => { + await PopupAutoClose.scheduleClose([]) + + // Should not call Settings.get or closeWindow + expect(mockSettings.get).not.toHaveBeenCalled() + expect(mockCloseWindow).not.toHaveBeenCalled() + }) + + it("should cancel existing timer before scheduling new one", async () => { + const delay = 1000 + const windows1 = [{ id: 100, commandId: "test1", srcWindowId: 1 }] + const windows2 = [{ id: 101, commandId: "test2", srcWindowId: 1 }] + + mockSettings.get.mockResolvedValue({ + popupAutoCloseDelay: delay, + } as any) + + // Schedule first close + await PopupAutoClose.scheduleClose(windows1) + + // Advance time halfway + await vi.advanceTimersByTimeAsync(delay / 2) + + // Schedule second close (should cancel first) + await PopupAutoClose.scheduleClose(windows2) + + // Advance remaining time + await vi.advanceTimersByTimeAsync(delay / 2) + + // First window should NOT be closed (timer was cancelled) + expect(mockCloseWindow).not.toHaveBeenCalledWith(100, "onFocusChanged") + + // Second window should not be closed yet (only half time passed) + expect(mockCloseWindow).not.toHaveBeenCalled() + + // Advance remaining time for second timer + await vi.advanceTimersByTimeAsync(delay / 2) + + // Now second window should be closed + expect(mockCloseWindow).toHaveBeenCalledWith(101, "onFocusChanged") + }) + }) +}) diff --git a/packages/extension/src/services/popupAutoClose.ts b/packages/extension/src/services/popupAutoClose.ts new file mode 100644 index 00000000..341bf34e --- /dev/null +++ b/packages/extension/src/services/popupAutoClose.ts @@ -0,0 +1,59 @@ +import { Settings } from "@/services/settings/settings" +import { closeWindow } from "@/services/chrome" +import { WindowStackManager } from "@/services/windowStackManager" +import type { WindowType } from "@/types" + +/** + * Popup Auto-Close Manager + * Manages automatic closing of popup windows with configurable delay + */ +export class PopupAutoClose { + private static timer: NodeJS.Timeout | null = null + + /** + * Cancel any pending auto-close timer + */ + static cancelTimer(): void { + if (this.timer !== null) { + clearTimeout(this.timer) + this.timer = null + } + } + + /** + * Schedule popup windows to close with configured delay + * @param windowsToClose - Array of windows to close + * @returns Promise that resolves when windows are closed or timer is set + */ + static async scheduleClose(windowsToClose: WindowType[]): Promise { + // Cancel any existing timer first + this.cancelTimer() + + // If no windows to close, nothing to do + if (windowsToClose.length === 0) { + return + } + + // Get the auto-close delay setting + const settings = await Settings.get() + const autoCloseDelay = settings.popupAutoCloseDelay + + // Define the close function + const closeWindows = async () => { + for (const window of windowsToClose) { + await closeWindow(window.id, "onFocusChanged") + await WindowStackManager.removeWindow(window.id) + } + this.timer = null + } + + // Execute close based on delay setting + if (autoCloseDelay !== undefined && autoCloseDelay > 0) { + // Delayed close: Set timeout + this.timer = setTimeout(closeWindows, autoCloseDelay) + } else { + // Immediate close: No delay configured + await closeWindows() + } + } +} From 85d6fbbbb5f4f329e1120a2c21a740552ff5f45e Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Sat, 21 Feb 2026 17:16:01 +0900 Subject: [PATCH 10/14] Update: Improve display in the SettingForm. --- .../public/_locales/de/messages.json | 15 +++ .../public/_locales/en/messages.json | 6 ++ .../public/_locales/es/messages.json | 15 +++ .../public/_locales/fr/messages.json | 15 +++ .../public/_locales/hi/messages.json | 15 +++ .../public/_locales/id/messages.json | 15 +++ .../public/_locales/it/messages.json | 15 +++ .../public/_locales/ja/messages.json | 10 +- .../public/_locales/ko/messages.json | 15 +++ .../public/_locales/ms/messages.json | 15 +++ .../public/_locales/pt_BR/messages.json | 15 +++ .../public/_locales/pt_PT/messages.json | 15 +++ .../public/_locales/ru/messages.json | 15 +++ .../public/_locales/zh_CN/messages.json | 15 +++ .../extension/src/background_script.test.ts | 46 +++------ .../src/components/option/SettingForm.tsx | 99 +++++++++++-------- .../src/components/option/TableOfContents.tsx | 4 + .../components/option/field/InputField.tsx | 29 +++++- .../src/services/option/defaultSettings.ts | 2 +- .../src/services/popupAutoClose.test.ts | 60 +++++++---- .../extension/src/services/popupAutoClose.ts | 9 +- packages/extension/src/types/index.ts | 2 +- 22 files changed, 348 insertions(+), 99 deletions(-) diff --git a/packages/extension/public/_locales/de/messages.json b/packages/extension/public/_locales/de/messages.json index a21d8a1d..10475ea9 100644 --- a/packages/extension/public/_locales/de/messages.json +++ b/packages/extension/public/_locales/de/messages.json @@ -461,6 +461,21 @@ "Option_showIndicator_desc": { "message": "Zeigt die verbleibende Zeit bis zur Vorschau an." }, + "Option_windowSettings": { + "message": "Fenstereinstellungen" + }, + "Option_windowSettings_desc": { + "message": "Konfigurieren Sie das Fensterverhalten und Popup-Einstellungen." + }, + "Option_popupAutoCloseDelay": { + "message": "Popup-Autoschluss-Verzögerung" + }, + "Option_popupAutoCloseDelay_desc": { + "message": "Stellen Sie die Verzögerungszeit ein, bevor das Popup nach dem Verlust des Fokus automatisch geschlossen wird. Stellen Sie 0 ein oder lassen Sie es leer für sofortiges Schließen.\nMaximum: 10000 ms" + }, + "Option_popupAutoCloseDelay_placeholder": { + "message": "0 (sofort schließen)" + }, "Option_folders": { "message": "Ordner" }, diff --git a/packages/extension/public/_locales/en/messages.json b/packages/extension/public/_locales/en/messages.json index 42ddaa4a..86f4ea4f 100644 --- a/packages/extension/public/_locales/en/messages.json +++ b/packages/extension/public/_locales/en/messages.json @@ -512,6 +512,12 @@ "Option_showIndicator_desc": { "message": "Displays the remaining to the preview." }, + "Option_windowSettings": { + "message": "Window Settings" + }, + "Option_windowSettings_desc": { + "message": "Configure window behavior and popup settings." + }, "Option_folders": { "message": "Folders" }, diff --git a/packages/extension/public/_locales/es/messages.json b/packages/extension/public/_locales/es/messages.json index fe25ac49..003912d3 100644 --- a/packages/extension/public/_locales/es/messages.json +++ b/packages/extension/public/_locales/es/messages.json @@ -461,6 +461,21 @@ "Option_showIndicator_desc": { "message": "Muestra el tiempo restante hasta la vista previa." }, + "Option_windowSettings": { + "message": "Configuración de Ventana" + }, + "Option_windowSettings_desc": { + "message": "Configure el comportamiento de la ventana y la configuración de ventanas emergentes." + }, + "Option_popupAutoCloseDelay": { + "message": "Retardo de Cierre Automático de Popup" + }, + "Option_popupAutoCloseDelay_desc": { + "message": "Establezca el tiempo de retardo antes de que el popup se cierre automáticamente después de perder el foco. Establezca 0 o déjelo vacío para cierre inmediato.\nMáximo: 10000 ms" + }, + "Option_popupAutoCloseDelay_placeholder": { + "message": "0 (cerrar inmediatamente)" + }, "Option_folders": { "message": "Carpetas" }, diff --git a/packages/extension/public/_locales/fr/messages.json b/packages/extension/public/_locales/fr/messages.json index 877041df..50133e3e 100644 --- a/packages/extension/public/_locales/fr/messages.json +++ b/packages/extension/public/_locales/fr/messages.json @@ -461,6 +461,21 @@ "Option_showIndicator_desc": { "message": "Affiche le temps restant jusqu'à l'aperçu." }, + "Option_windowSettings": { + "message": "Paramètres de Fenêtre" + }, + "Option_windowSettings_desc": { + "message": "Configurez le comportement de la fenêtre et les paramètres de popup." + }, + "Option_popupAutoCloseDelay": { + "message": "Délai de Fermeture Automatique du Popup" + }, + "Option_popupAutoCloseDelay_desc": { + "message": "Définissez le délai avant que le popup se ferme automatiquement après avoir perdu le focus. Définissez 0 ou laissez vide pour une fermeture immédiate.\nMaximum: 10000 ms" + }, + "Option_popupAutoCloseDelay_placeholder": { + "message": "0 (fermer immédiatement)" + }, "Option_folders": { "message": "Dossiers" }, diff --git a/packages/extension/public/_locales/hi/messages.json b/packages/extension/public/_locales/hi/messages.json index 53333a7e..1e4640d3 100644 --- a/packages/extension/public/_locales/hi/messages.json +++ b/packages/extension/public/_locales/hi/messages.json @@ -461,6 +461,21 @@ "Option_showIndicator_desc": { "message": "प्रीव्यू तक शेष समय दिखाएं।" }, + "Option_windowSettings": { + "message": "विंडो सेटिंग्स" + }, + "Option_windowSettings_desc": { + "message": "विंडो व्यवहार और पॉपअप सेटिंग्स को कॉन्फ़िगर करें।" + }, + "Option_popupAutoCloseDelay": { + "message": "पॉपअप ऑटो-क्लोज़ विलंब" + }, + "Option_popupAutoCloseDelay_desc": { + "message": "फोकस खोने के बाद पॉपअप के स्वचालित रूप से बंद होने से पहले विलंब का समय सेट करें। तुरंत बंद करने के लिए 0 सेट करें या खाली छोड़ दें।\nअधिकतम: 10000 ms" + }, + "Option_popupAutoCloseDelay_placeholder": { + "message": "0 (तुरंत बंद करें)" + }, "Option_folders": { "message": "फ़ोल्डर्स" }, diff --git a/packages/extension/public/_locales/id/messages.json b/packages/extension/public/_locales/id/messages.json index 2f5e66dd..b3685a0f 100644 --- a/packages/extension/public/_locales/id/messages.json +++ b/packages/extension/public/_locales/id/messages.json @@ -461,6 +461,21 @@ "Option_showIndicator_desc": { "message": "Menampilkan sisa ke pratinjau." }, + "Option_windowSettings": { + "message": "Pengaturan Jendela" + }, + "Option_windowSettings_desc": { + "message": "Konfigurasi perilaku jendela dan pengaturan popup." + }, + "Option_popupAutoCloseDelay": { + "message": "Penundaan Penutupan Otomatis Popup" + }, + "Option_popupAutoCloseDelay_desc": { + "message": "Atur waktu penundaan sebelum popup secara otomatis menutup setelah kehilangan fokus. Atur ke 0 atau biarkan kosong untuk penutupan segera.\nMaksimum: 10000 ms" + }, + "Option_popupAutoCloseDelay_placeholder": { + "message": "0 (tutup segera)" + }, "Option_folders": { "message": "Folder" }, diff --git a/packages/extension/public/_locales/it/messages.json b/packages/extension/public/_locales/it/messages.json index 2f72f722..97b4c3e6 100644 --- a/packages/extension/public/_locales/it/messages.json +++ b/packages/extension/public/_locales/it/messages.json @@ -461,6 +461,21 @@ "Option_showIndicator_desc": { "message": "Mostra il tempo rimanente fino all'anteprima." }, + "Option_windowSettings": { + "message": "Impostazioni Finestra" + }, + "Option_windowSettings_desc": { + "message": "Configura il comportamento della finestra e le impostazioni popup." + }, + "Option_popupAutoCloseDelay": { + "message": "Ritardo Chiusura Automatica Popup" + }, + "Option_popupAutoCloseDelay_desc": { + "message": "Imposta il tempo di ritardo prima che il popup si chiuda automaticamente dopo aver perso il focus. Imposta 0 o lascia vuoto per la chiusura immediata.\nMassimo: 10000 ms" + }, + "Option_popupAutoCloseDelay_placeholder": { + "message": "0 (chiudi immediatamente)" + }, "Option_folders": { "message": "Cartelle" }, diff --git a/packages/extension/public/_locales/ja/messages.json b/packages/extension/public/_locales/ja/messages.json index bb6819c7..69b3350f 100644 --- a/packages/extension/public/_locales/ja/messages.json +++ b/packages/extension/public/_locales/ja/messages.json @@ -204,10 +204,10 @@ "message": "メニュー表示アニメーション" }, "Option_popupAutoCloseDelay": { - "message": "ポップアップ自動クローズまでの遅延時間" + "message": "ポップアップ自動クローズまでの時間" }, "Option_popupAutoCloseDelay_desc": { - "message": "フォーカスが外れてからポップアップが自動的に閉じるまでの時間を設定します。0または未設定の場合は即座に閉じます。" + "message": "ポップアップウィンドウのフォーカスが外れてから自動的に閉じるまでの時間を設定します。0または未設定の場合は即座に閉じます。 \n最大:10000 ms" }, "Option_popupAutoCloseDelay_placeholder": { "message": "0 (即座に閉じる)" @@ -599,6 +599,12 @@ "Option_showIndicator_desc": { "message": "プレビューまでの残りを表示します。" }, + "Option_windowSettings": { + "message": "ウィンドウ設定" + }, + "Option_windowSettings_desc": { + "message": "ウィンドウやポップアップの設定を行います。" + }, "Option_folders": { "message": "フォルダ" }, diff --git a/packages/extension/public/_locales/ko/messages.json b/packages/extension/public/_locales/ko/messages.json index f680f23f..3dd5dea4 100644 --- a/packages/extension/public/_locales/ko/messages.json +++ b/packages/extension/public/_locales/ko/messages.json @@ -461,6 +461,21 @@ "Option_showIndicator_desc": { "message": "미리보기까지 남은 시간을 표시합니다." }, + "Option_windowSettings": { + "message": "창 설정" + }, + "Option_windowSettings_desc": { + "message": "창 동작 및 팝업 설정을 구성합니다." + }, + "Option_popupAutoCloseDelay": { + "message": "팝업 자동 닫기 지연" + }, + "Option_popupAutoCloseDelay_desc": { + "message": "포커스를 잃은 후 팝업이 자동으로 닫히기 전 지연 시간을 설정합니다. 즉시 닫으려면 0으로 설정하거나 비워 두십시오.\n최대: 10000 ms" + }, + "Option_popupAutoCloseDelay_placeholder": { + "message": "0 (즉시 닫기)" + }, "Option_folders": { "message": "폴더" }, diff --git a/packages/extension/public/_locales/ms/messages.json b/packages/extension/public/_locales/ms/messages.json index 01a3d6ec..3fc0f4b9 100644 --- a/packages/extension/public/_locales/ms/messages.json +++ b/packages/extension/public/_locales/ms/messages.json @@ -461,6 +461,21 @@ "Option_showIndicator_desc": { "message": "Memaparkan baki ke pratonton." }, + "Option_windowSettings": { + "message": "Tetapan Tetingkap" + }, + "Option_windowSettings_desc": { + "message": "Konfigurasi tingkah laku tetingkap dan tetapan popup." + }, + "Option_popupAutoCloseDelay": { + "message": "Kelewatan Penutupan Auto Popup" + }, + "Option_popupAutoCloseDelay_desc": { + "message": "Tetapkan masa kelewatan sebelum popup menutup secara automatik selepas kehilangan fokus. Tetapkan ke 0 atau biarkan kosong untuk penutupan segera.\nMaksimum: 10000 ms" + }, + "Option_popupAutoCloseDelay_placeholder": { + "message": "0 (tutup segera)" + }, "Option_folders": { "message": "Folder" }, diff --git a/packages/extension/public/_locales/pt_BR/messages.json b/packages/extension/public/_locales/pt_BR/messages.json index 60d3069b..54b60b85 100644 --- a/packages/extension/public/_locales/pt_BR/messages.json +++ b/packages/extension/public/_locales/pt_BR/messages.json @@ -461,6 +461,21 @@ "Option_showIndicator_desc": { "message": "Exibe o restante para a visualização." }, + "Option_windowSettings": { + "message": "Configurações da Janela" + }, + "Option_windowSettings_desc": { + "message": "Configure o comportamento da janela e as configurações de popup." + }, + "Option_popupAutoCloseDelay": { + "message": "Atraso de Fechamento Automático do Popup" + }, + "Option_popupAutoCloseDelay_desc": { + "message": "Defina o tempo de atraso antes que o popup feche automaticamente após perder o foco. Defina como 0 ou deixe vazio para fechamento imediato.\nMáximo: 10000 ms" + }, + "Option_popupAutoCloseDelay_placeholder": { + "message": "0 (fechar imediatamente)" + }, "Option_folders": { "message": "Pastas" }, diff --git a/packages/extension/public/_locales/pt_PT/messages.json b/packages/extension/public/_locales/pt_PT/messages.json index 90f83ec5..5a5796b7 100644 --- a/packages/extension/public/_locales/pt_PT/messages.json +++ b/packages/extension/public/_locales/pt_PT/messages.json @@ -461,6 +461,21 @@ "Option_showIndicator_desc": { "message": "Exibe o restante para a visualização." }, + "Option_windowSettings": { + "message": "Definições da Janela" + }, + "Option_windowSettings_desc": { + "message": "Configure o comportamento da janela e as definições de popup." + }, + "Option_popupAutoCloseDelay": { + "message": "Atraso de Fecho Automático do Popup" + }, + "Option_popupAutoCloseDelay_desc": { + "message": "Defina o tempo de atraso antes que o popup feche automaticamente após perder o foco. Defina como 0 ou deixe vazio para fecho imediato.\nMáximo: 10000 ms" + }, + "Option_popupAutoCloseDelay_placeholder": { + "message": "0 (fechar imediatamente)" + }, "Option_folders": { "message": "Pastas" }, diff --git a/packages/extension/public/_locales/ru/messages.json b/packages/extension/public/_locales/ru/messages.json index 4e8b3a8e..ffc12c76 100644 --- a/packages/extension/public/_locales/ru/messages.json +++ b/packages/extension/public/_locales/ru/messages.json @@ -461,6 +461,21 @@ "Option_showIndicator_desc": { "message": "Показывать оставшееся время до предпросмотра." }, + "Option_windowSettings": { + "message": "Настройки Окна" + }, + "Option_windowSettings_desc": { + "message": "Настройте поведение окна и параметры всплывающих окон." + }, + "Option_popupAutoCloseDelay": { + "message": "Задержка Автозакрытия Всплывающего Окна" + }, + "Option_popupAutoCloseDelay_desc": { + "message": "Установите время задержки перед автоматическим закрытием всплывающего окна после потери фокуса. Установите 0 или оставьте пустым для немедленного закрытия.\nМаксимум: 10000 мс" + }, + "Option_popupAutoCloseDelay_placeholder": { + "message": "0 (закрыть немедленно)" + }, "Option_folders": { "message": "Папки" }, diff --git a/packages/extension/public/_locales/zh_CN/messages.json b/packages/extension/public/_locales/zh_CN/messages.json index 89dea19d..6a8c2d1f 100644 --- a/packages/extension/public/_locales/zh_CN/messages.json +++ b/packages/extension/public/_locales/zh_CN/messages.json @@ -461,6 +461,21 @@ "Option_showIndicator_desc": { "message": "显示预览前的剩余时间。" }, + "Option_windowSettings": { + "message": "窗口设置" + }, + "Option_windowSettings_desc": { + "message": "配置窗口行为和弹出窗口设置。" + }, + "Option_popupAutoCloseDelay": { + "message": "弹出窗口自动关闭延迟" + }, + "Option_popupAutoCloseDelay_desc": { + "message": "设置弹出窗口失去焦点后自动关闭前的延迟时间。设置为0或留空表示立即关闭。\n最大值:10000毫秒" + }, + "Option_popupAutoCloseDelay_placeholder": { + "message": "0(立即关闭)" + }, "Option_folders": { "message": "文件夹" }, diff --git a/packages/extension/src/background_script.test.ts b/packages/extension/src/background_script.test.ts index 328a7e40..93c9aa3e 100644 --- a/packages/extension/src/background_script.test.ts +++ b/packages/extension/src/background_script.test.ts @@ -7,6 +7,7 @@ import { POPUP_ENABLED, LINK_COMMAND_ENABLED } from "@/const" // Mock dependencies vi.mock("@/services/settings/enhancedSettings") vi.mock("@/services/settings/settings") +vi.mock("@/services/settings/settingsCache") vi.mock("@/services/storage") vi.mock("@/services/chrome") vi.mock("@/services/backgroundData") @@ -410,16 +411,11 @@ describe("Popup Auto-Close Delay", () => { windowExists: vi.fn(), })) - // Mock Settings to return no delay - mockSettings.get.mockResolvedValue({ - popupAutoCloseDelay: undefined, - commands: [], - folders: [], - pageRules: [], - stars: [], - shortcuts: { shortcuts: [] }, - commandExecutionCount: 0, - hasShownReviewRequest: false, + // Mock enhancedSettings to return no delay + mockEnhancedSettings.getSection.mockResolvedValue({ + windowOption: { + popupAutoCloseDelay: undefined, + }, } as any) // Mock Storage @@ -482,16 +478,11 @@ describe("Popup Auto-Close Delay", () => { windowExists: vi.fn(), })) - // Mock Settings to return delay - mockSettings.get.mockResolvedValue({ - popupAutoCloseDelay: delay, - commands: [], - folders: [], - pageRules: [], - stars: [], - shortcuts: { shortcuts: [] }, - commandExecutionCount: 0, - hasShownReviewRequest: false, + // Mock enhancedSettings to return delay + mockEnhancedSettings.getSection.mockResolvedValue({ + windowOption: { + popupAutoCloseDelay: delay, + }, } as any) // Mock Storage @@ -557,16 +548,11 @@ describe("Popup Auto-Close Delay", () => { windowExists: vi.fn(), })) - // Mock Settings to return delay - mockSettings.get.mockResolvedValue({ - popupAutoCloseDelay: delay, - commands: [], - folders: [], - pageRules: [], - stars: [], - shortcuts: { shortcuts: [] }, - commandExecutionCount: 0, - hasShownReviewRequest: false, + // Mock enhancedSettings to return delay + mockEnhancedSettings.getSection.mockResolvedValue({ + windowOption: { + popupAutoCloseDelay: delay, + }, } as any) // Mock Storage diff --git a/packages/extension/src/components/option/SettingForm.tsx b/packages/extension/src/components/option/SettingForm.tsx index 0c04a031..8c8d6316 100644 --- a/packages/extension/src/components/option/SettingForm.tsx +++ b/packages/extension/src/components/option/SettingForm.tsx @@ -9,6 +9,7 @@ import { Eye, BookOpen, Paintbrush, + AppWindow, } from "lucide-react" import { Form } from "@/components/ui/form" @@ -79,11 +80,16 @@ const formSchema = z }) .strict(), popupPlacement: popupPlacementSchema, - popupAutoCloseDelay: z - .number({ message: t("zod_number") }) - .min(0, { message: t("zod_number_min", ["0"]) }) - .max(10000, { message: t("zod_number_max", ["10000"]) }) - .optional(), + windowOption: z + .object({ + sidePanelAutoHide: z.boolean(), + popupAutoCloseDelay: z + .number({ message: t("zod_number") }) + .min(0, { message: t("zod_number_min", ["0"]) }) + .max(10000, { message: t("zod_number_max", ["10000"]) }) + .optional(), + }) + .strict(), style: z.nativeEnum(STYLE), commands: z.array(commandSchema).min(1), folders: z.array(folderSchema), @@ -134,7 +140,7 @@ export function SettingForm({ className }: { className?: string }) { defaultValues: { startupMethod: emptySettings.startupMethod, popupPlacement: emptySettings.popupPlacement, - popupAutoCloseDelay: emptySettings.popupAutoCloseDelay, + windowOption: emptySettings.windowOption, style: emptySettings.style, commands: [], // Empty array to avoid type conflicts folders: emptySettings.folders, @@ -470,26 +476,6 @@ export function SettingForm({ className }: { className?: string }) { defaultValues={getAnimationDefaultValues()} /> )} - - {startupMethod !== STARTUP_METHOD.CONTEXT_MENU && ( - (v === "" ? undefined : Number(v)), - }), - }} - /> - )}

@@ -575,31 +561,60 @@ export function SettingForm({ className }: { className?: string }) { )} {linkCommandMethod === LINK_COMMAND_STARTUP_METHOD.LEFT_CLICK_HOLD && ( + + )} + +
+
+ +
+

+ + {t("windowSettings")} +

+

{t("windowSettings_desc")}

+ + {startupMethod !== STARTUP_METHOD.CONTEXT_MENU && ( (v === "" ? undefined : Number(v)), }), }} /> )} -

diff --git a/packages/extension/src/components/option/TableOfContents.tsx b/packages/extension/src/components/option/TableOfContents.tsx index c877f147..8d5be705 100644 --- a/packages/extension/src/components/option/TableOfContents.tsx +++ b/packages/extension/src/components/option/TableOfContents.tsx @@ -7,6 +7,7 @@ import { BookOpen, Paintbrush, Keyboard, + AppWindow, } from "lucide-react" import styles from "./TableOfContents.module.css" import optionCss from "./Option.module.css" @@ -21,6 +22,7 @@ export const TableOfContents = (props: Props) => { "commands", "shortcuts", "linkCommand", + "windowSettings", "pageRules", "userStyles", ] @@ -73,6 +75,8 @@ const Icon = ({ return case "shortcuts": return + case "windowSettings": + return case "pageRules": return case "userStyles": diff --git a/packages/extension/src/components/option/field/InputField.tsx b/packages/extension/src/components/option/field/InputField.tsx index faecc1ef..f28cb8a3 100644 --- a/packages/extension/src/components/option/field/InputField.tsx +++ b/packages/extension/src/components/option/field/InputField.tsx @@ -1,3 +1,4 @@ +import { useRef } from "react" import { Input } from "@/components/ui/input" import { FormControl, @@ -8,6 +9,8 @@ import { FormDescription, } from "@/components/ui/form" import { MenuImage } from "@/components/menu/MenuImage" +import { Info } from "lucide-react" +import { Tooltip } from "@/components/Tooltip" type InputFieldType = { control: any @@ -16,6 +19,7 @@ type InputFieldType = { inputProps: React.ComponentProps unit?: string description?: string + tooltip?: string previewUrl?: string } @@ -28,9 +32,12 @@ export const InputField = ({ inputProps, unit, description, + tooltip, previewUrl, }: InputFieldType) => { const hasPreview = !isEmpty(previewUrl) + const span = useRef(null) + return ( (
- {formLabel} + + {formLabel} + {tooltip && ( + + + + )} + {description && {description}} + {tooltip && ( + + )}
{hasPreview && ( diff --git a/packages/extension/src/services/option/defaultSettings.ts b/packages/extension/src/services/option/defaultSettings.ts index 46c804bc..0caa3772 100644 --- a/packages/extension/src/services/option/defaultSettings.ts +++ b/packages/extension/src/services/option/defaultSettings.ts @@ -36,7 +36,6 @@ export const emptySettings: SettingsType = { alignOffset: 0, sideOffset: 0, }, - popupAutoCloseDelay: undefined, linkCommand: { enabled: LINK_COMMAND_ENABLED.ENABLE, openMode: DRAG_OPEN_MODE.PREVIEW_POPUP, @@ -57,6 +56,7 @@ export const emptySettings: SettingsType = { shortcuts: { shortcuts: [] }, windowOption: { sidePanelAutoHide: false, + popupAutoCloseDelay: undefined, }, } diff --git a/packages/extension/src/services/popupAutoClose.test.ts b/packages/extension/src/services/popupAutoClose.test.ts index ffbcc382..63566ba5 100644 --- a/packages/extension/src/services/popupAutoClose.test.ts +++ b/packages/extension/src/services/popupAutoClose.test.ts @@ -1,15 +1,27 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" + +// Mock all dependencies before importing PopupAutoClose +vi.mock("@/services/settings/enhancedSettings", () => ({ + enhancedSettings: { + getSection: vi.fn(), + }, +})) +vi.mock("@/services/settings/settingsCache") +vi.mock("@/services/chrome", () => ({ + closeWindow: vi.fn(), +})) +vi.mock("@/services/windowStackManager", () => ({ + WindowStackManager: { + removeWindow: vi.fn(), + }, +})) + import { PopupAutoClose } from "./popupAutoClose" -import { Settings } from "@/services/settings/settings" +import { enhancedSettings } from "@/services/settings/enhancedSettings" import { closeWindow } from "@/services/chrome" import { WindowStackManager } from "@/services/windowStackManager" -// Mock dependencies -vi.mock("@/services/settings/settings") -vi.mock("@/services/chrome") -vi.mock("@/services/windowStackManager") - -const mockSettings = vi.mocked(Settings) +const mockEnhancedSettings = vi.mocked(enhancedSettings) const mockCloseWindow = vi.mocked(closeWindow) const mockWindowStackManager = vi.mocked(WindowStackManager) @@ -34,8 +46,10 @@ describe("PopupAutoClose", () => { it("should close windows immediately when delay is undefined", async () => { const windows = [{ id: 100, commandId: "test", srcWindowId: 1 }] - mockSettings.get.mockResolvedValue({ - popupAutoCloseDelay: undefined, + mockEnhancedSettings.getSection.mockResolvedValue({ + windowOption: { + popupAutoCloseDelay: undefined, + }, } as any) await PopupAutoClose.scheduleClose(windows) @@ -48,8 +62,10 @@ describe("PopupAutoClose", () => { it("should close windows immediately when delay is 0", async () => { const windows = [{ id: 100, commandId: "test", srcWindowId: 1 }] - mockSettings.get.mockResolvedValue({ - popupAutoCloseDelay: 0, + mockEnhancedSettings.getSection.mockResolvedValue({ + windowOption: { + popupAutoCloseDelay: 0, + }, } as any) await PopupAutoClose.scheduleClose(windows) @@ -63,8 +79,10 @@ describe("PopupAutoClose", () => { const delay = 1000 const windows = [{ id: 100, commandId: "test", srcWindowId: 1 }] - mockSettings.get.mockResolvedValue({ - popupAutoCloseDelay: delay, + mockEnhancedSettings.getSection.mockResolvedValue({ + windowOption: { + popupAutoCloseDelay: delay, + }, } as any) await PopupAutoClose.scheduleClose(windows) @@ -86,8 +104,10 @@ describe("PopupAutoClose", () => { { id: 101, commandId: "test2", srcWindowId: 1 }, ] - mockSettings.get.mockResolvedValue({ - popupAutoCloseDelay: undefined, + mockEnhancedSettings.getSection.mockResolvedValue({ + windowOption: { + popupAutoCloseDelay: undefined, + }, } as any) await PopupAutoClose.scheduleClose(windows) @@ -102,8 +122,8 @@ describe("PopupAutoClose", () => { it("should do nothing when no windows to close", async () => { await PopupAutoClose.scheduleClose([]) - // Should not call Settings.get or closeWindow - expect(mockSettings.get).not.toHaveBeenCalled() + // Should not call enhancedSettings.getSection or closeWindow + expect(mockEnhancedSettings.getSection).not.toHaveBeenCalled() expect(mockCloseWindow).not.toHaveBeenCalled() }) @@ -112,8 +132,10 @@ describe("PopupAutoClose", () => { const windows1 = [{ id: 100, commandId: "test1", srcWindowId: 1 }] const windows2 = [{ id: 101, commandId: "test2", srcWindowId: 1 }] - mockSettings.get.mockResolvedValue({ - popupAutoCloseDelay: delay, + mockEnhancedSettings.getSection.mockResolvedValue({ + windowOption: { + popupAutoCloseDelay: delay, + }, } as any) // Schedule first close diff --git a/packages/extension/src/services/popupAutoClose.ts b/packages/extension/src/services/popupAutoClose.ts index 341bf34e..cdbb64ac 100644 --- a/packages/extension/src/services/popupAutoClose.ts +++ b/packages/extension/src/services/popupAutoClose.ts @@ -1,4 +1,5 @@ -import { Settings } from "@/services/settings/settings" +import { enhancedSettings } from "@/services/settings/enhancedSettings" +import { CACHE_SECTIONS } from "@/services/settings/settingsCache" import { closeWindow } from "@/services/chrome" import { WindowStackManager } from "@/services/windowStackManager" import type { WindowType } from "@/types" @@ -35,8 +36,10 @@ export class PopupAutoClose { } // Get the auto-close delay setting - const settings = await Settings.get() - const autoCloseDelay = settings.popupAutoCloseDelay + const userSettings = await enhancedSettings.getSection( + CACHE_SECTIONS.USER_SETTINGS, + ) + const autoCloseDelay = userSettings.windowOption.popupAutoCloseDelay // Define the close function const closeWindows = async () => { diff --git a/packages/extension/src/types/index.ts b/packages/extension/src/types/index.ts index d2a9ab4b..e7f3b96d 100644 --- a/packages/extension/src/types/index.ts +++ b/packages/extension/src/types/index.ts @@ -79,6 +79,7 @@ export type CopyOption = "default" | "text" type WindowOption = { sidePanelAutoHide: boolean + popupAutoCloseDelay?: number } type LinkCommandStartupMethod = { @@ -169,7 +170,6 @@ export type UserSettings = { settingVersion: Version startupMethod: StartupMethod popupPlacement: PopupPlacement - popupAutoCloseDelay?: number commands: Array linkCommand: LinkCommandSettings folders: Array From bb9e0e5e1c7d52bbdee6c8a2a819162efc6fbf16 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Sat, 21 Feb 2026 17:30:58 +0900 Subject: [PATCH 11/14] Update: Add delay even in onHidden. --- packages/extension/src/background_script.ts | 8 +++++--- packages/shared/tsconfig.tsbuildinfo | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/extension/src/background_script.ts b/packages/extension/src/background_script.ts index 2d463874..e53955fc 100644 --- a/packages/extension/src/background_script.ts +++ b/packages/extension/src/background_script.ts @@ -288,9 +288,11 @@ const commandFuncs = { return } - // Remove the window. - await closeWindow(windowId, "onHidden") - await WindowStackManager.removeWindow(windowId) + // Schedule popup window to close with configured delay + const window = layer.find((w) => w.id === windowId) + if (window) { + await PopupAutoClose.scheduleClose([window]) + } response(false) } diff --git a/packages/shared/tsconfig.tsbuildinfo b/packages/shared/tsconfig.tsbuildinfo index b3c515d0..64f82187 100644 --- a/packages/shared/tsconfig.tsbuildinfo +++ b/packages/shared/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/index.ts","./src/constants/index.ts","./src/constants/open-mode.ts","./src/types/command.ts","./src/types/common.ts","./src/types/index.ts","./src/utils/cn.ts","./src/utils/common.ts","./src/utils/index.ts","./src/utils/type-guards.ts"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/index.ts","./src/constants/index.ts","./src/constants/open-mode.ts","./src/types/command.ts","./src/types/common.ts","./src/types/index.ts","./src/utils/cn.ts","./src/utils/common.ts","./src/utils/index.ts","./src/utils/type-guards.ts"],"version":"5.9.2"} \ No newline at end of file From e1623afd0c87d0c06d203b04e0f8e27c3f30338b Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Sun, 22 Feb 2026 14:29:04 +0900 Subject: [PATCH 12/14] Update packages/extension/public/_locales/en/messages.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/extension/public/_locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension/public/_locales/en/messages.json b/packages/extension/public/_locales/en/messages.json index 86f4ea4f..fc3f0541 100644 --- a/packages/extension/public/_locales/en/messages.json +++ b/packages/extension/public/_locales/en/messages.json @@ -207,7 +207,7 @@ "message": "Popup Auto-Close Delay" }, "Option_popupAutoCloseDelay_desc": { - "message": "Set the delay time before the popup automatically closes after losing focus. Set to 0 or leave empty for immediate close." + "message": "Set the delay time before the popup automatically closes after losing focus. Set to 0 or leave empty for immediate close.\nMaximum: 10000 ms" }, "Option_popupAutoCloseDelay_placeholder": { "message": "0 (close immediately)" From 9ca073f17544330ac983d92ad375bdb20f6587f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 05:42:29 +0000 Subject: [PATCH 13/14] Fix timer type, add reason parameter, error handling, and use 0 as default Co-authored-by: ujiro99 <677231+ujiro99@users.noreply.github.com> --- packages/extension/src/background_script.ts | 2 +- .../src/services/option/defaultSettings.ts | 3 +- .../src/services/popupAutoClose.test.ts | 38 ++++++++++++++++++- .../extension/src/services/popupAutoClose.ts | 19 +++++++--- 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/packages/extension/src/background_script.ts b/packages/extension/src/background_script.ts index e53955fc..d6eb43eb 100644 --- a/packages/extension/src/background_script.ts +++ b/packages/extension/src/background_script.ts @@ -291,7 +291,7 @@ const commandFuncs = { // Schedule popup window to close with configured delay const window = layer.find((w) => w.id === windowId) if (window) { - await PopupAutoClose.scheduleClose([window]) + await PopupAutoClose.scheduleClose([window], "onHidden") } response(false) } diff --git a/packages/extension/src/services/option/defaultSettings.ts b/packages/extension/src/services/option/defaultSettings.ts index 0caa3772..1f8533c1 100644 --- a/packages/extension/src/services/option/defaultSettings.ts +++ b/packages/extension/src/services/option/defaultSettings.ts @@ -56,7 +56,7 @@ export const emptySettings: SettingsType = { shortcuts: { shortcuts: [] }, windowOption: { sidePanelAutoHide: false, - popupAutoCloseDelay: undefined, + popupAutoCloseDelay: 0, }, } @@ -159,6 +159,7 @@ export default { }, windowOption: { sidePanelAutoHide: false, + popupAutoCloseDelay: 0, }, } as UserSettings diff --git a/packages/extension/src/services/popupAutoClose.test.ts b/packages/extension/src/services/popupAutoClose.test.ts index 63566ba5..4882b381 100644 --- a/packages/extension/src/services/popupAutoClose.test.ts +++ b/packages/extension/src/services/popupAutoClose.test.ts @@ -162,5 +162,41 @@ describe("PopupAutoClose", () => { // Now second window should be closed expect(mockCloseWindow).toHaveBeenCalledWith(101, "onFocusChanged") }) + + it("should use custom reason when provided", async () => { + const windows = [{ id: 100, commandId: "test", srcWindowId: 1 }] + + mockEnhancedSettings.getSection.mockResolvedValue({ + windowOption: { + popupAutoCloseDelay: 0, + }, + } as any) + + await PopupAutoClose.scheduleClose(windows, "onHidden") + + // Should close with custom reason + expect(mockCloseWindow).toHaveBeenCalledWith(100, "onHidden") + }) + + it("should handle errors and still clear timer", async () => { + const windows = [ + { id: 100, commandId: "test1", srcWindowId: 1 }, + { id: 101, commandId: "test2", srcWindowId: 1 }, + ] + + mockEnhancedSettings.getSection.mockResolvedValue({ + windowOption: { + popupAutoCloseDelay: 0, + }, + } as any) + + // Make first closeWindow call fail + mockCloseWindow.mockRejectedValueOnce(new Error("Close failed")) + + await expect(PopupAutoClose.scheduleClose(windows)).rejects.toThrow("Close failed") + + // Timer should still be cleared even after error + expect(mockCloseWindow).toHaveBeenCalledTimes(1) + }) }) -}) +}) \ No newline at end of file diff --git a/packages/extension/src/services/popupAutoClose.ts b/packages/extension/src/services/popupAutoClose.ts index cdbb64ac..15f69c73 100644 --- a/packages/extension/src/services/popupAutoClose.ts +++ b/packages/extension/src/services/popupAutoClose.ts @@ -9,7 +9,7 @@ import type { WindowType } from "@/types" * Manages automatic closing of popup windows with configurable delay */ export class PopupAutoClose { - private static timer: NodeJS.Timeout | null = null + private static timer: ReturnType | null = null /** * Cancel any pending auto-close timer @@ -24,9 +24,13 @@ export class PopupAutoClose { /** * Schedule popup windows to close with configured delay * @param windowsToClose - Array of windows to close + * @param reason - Reason for closing (e.g., "onFocusChanged", "onHidden") * @returns Promise that resolves when windows are closed or timer is set */ - static async scheduleClose(windowsToClose: WindowType[]): Promise { + static async scheduleClose( + windowsToClose: WindowType[], + reason: string = "onFocusChanged", + ): Promise { // Cancel any existing timer first this.cancelTimer() @@ -43,11 +47,14 @@ export class PopupAutoClose { // Define the close function const closeWindows = async () => { - for (const window of windowsToClose) { - await closeWindow(window.id, "onFocusChanged") - await WindowStackManager.removeWindow(window.id) + try { + for (const window of windowsToClose) { + await closeWindow(window.id, reason) + await WindowStackManager.removeWindow(window.id) + } + } finally { + this.timer = null } - this.timer = null } // Execute close based on delay setting From 9d0bcf009d2ffabe38748a1df94210fea31cbab9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 05:43:37 +0000 Subject: [PATCH 14/14] Use typed CloseReason and improve error handling test Co-authored-by: ujiro99 <677231+ujiro99@users.noreply.github.com> --- packages/extension/src/services/popupAutoClose.test.ts | 10 ++++++++-- packages/extension/src/services/popupAutoClose.ts | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/extension/src/services/popupAutoClose.test.ts b/packages/extension/src/services/popupAutoClose.test.ts index 4882b381..a5159a2a 100644 --- a/packages/extension/src/services/popupAutoClose.test.ts +++ b/packages/extension/src/services/popupAutoClose.test.ts @@ -195,8 +195,14 @@ describe("PopupAutoClose", () => { await expect(PopupAutoClose.scheduleClose(windows)).rejects.toThrow("Close failed") - // Timer should still be cleared even after error - expect(mockCloseWindow).toHaveBeenCalledTimes(1) + // Timer should still be cleared - verify by scheduling another close + mockCloseWindow.mockResolvedValue(undefined) + const windows2 = [{ id: 102, commandId: "test3", srcWindowId: 1 }] + + await PopupAutoClose.scheduleClose(windows2) + + // Should work fine, proving timer was cleared + expect(mockCloseWindow).toHaveBeenCalledWith(102, "onFocusChanged") }) }) }) \ No newline at end of file diff --git a/packages/extension/src/services/popupAutoClose.ts b/packages/extension/src/services/popupAutoClose.ts index 15f69c73..a1d87bab 100644 --- a/packages/extension/src/services/popupAutoClose.ts +++ b/packages/extension/src/services/popupAutoClose.ts @@ -4,6 +4,11 @@ import { closeWindow } from "@/services/chrome" import { WindowStackManager } from "@/services/windowStackManager" import type { WindowType } from "@/types" +/** + * Valid reasons for closing popup windows + */ +export type CloseReason = "onFocusChanged" | "onHidden" + /** * Popup Auto-Close Manager * Manages automatic closing of popup windows with configurable delay @@ -29,7 +34,7 @@ export class PopupAutoClose { */ static async scheduleClose( windowsToClose: WindowType[], - reason: string = "onFocusChanged", + reason: CloseReason = "onFocusChanged", ): Promise { // Cancel any existing timer first this.cancelTimer()