From c7ae827fa6aad640c7176ede52eeb3c5bad25df7 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Sat, 21 Mar 2026 15:53:46 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Update:=20Limit=20functionality=20since=20c?= =?UTF-8?q?lipboard=20access=20doesn=E2=80=99t=20work=20properly=20in=20Si?= =?UTF-8?q?dePanel=20mode.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/extension/src/action/background.ts | 4 +- packages/extension/src/background_script.ts | 64 +++++++-------- .../components/option/editor/ShortcutList.tsx | 77 ++++++++++++++----- packages/extension/src/const.ts | 1 - packages/extension/src/services/chrome.ts | 12 ++- packages/extension/src/services/screen.ts | 11 ++- 6 files changed, 101 insertions(+), 68 deletions(-) diff --git a/packages/extension/src/action/background.ts b/packages/extension/src/action/background.ts index 3a6f2bdc..b9a22711 100644 --- a/packages/extension/src/action/background.ts +++ b/packages/extension/src/action/background.ts @@ -3,19 +3,17 @@ import { Popup } from "./popup" import { Window } from "./window" import { Tab } from "./tab" import { BackgroundTab } from "./backgroundTab" -import { SidePanel } from "./sidePanel" import { Api } from "./api" import { PageAction } from "./pageAction" import { AiPrompt } from "./aiPrompt" import { executeAction } from "./executor" import type { ExecuteCommandParams } from "@/types" -export const actionsForBackground = { +const actionsForBackground = { [OPEN_MODE_BG.POPUP]: Popup, [OPEN_MODE_BG.WINDOW]: Window, [OPEN_MODE_BG.TAB]: Tab, [OPEN_MODE_BG.BACKGROUND_TAB]: BackgroundTab, - [OPEN_MODE_BG.SIDE_PANEL]: SidePanel, [OPEN_MODE_BG.API]: Api, [OPEN_MODE_BG.PAGE_ACTION]: PageAction, [OPEN_MODE_BG.AI_PROMPT]: AiPrompt, diff --git a/packages/extension/src/background_script.ts b/packages/extension/src/background_script.ts index 409c41e8..7284130e 100644 --- a/packages/extension/src/background_script.ts +++ b/packages/extension/src/background_script.ts @@ -60,7 +60,7 @@ const getActiveTabId = ( return true } -const onConnect = async function(port: chrome.runtime.Port) { +const onConnect = async function (port: chrome.runtime.Port) { if (port.name !== CONNECTION_APP) return port.onDisconnect.addListener(() => onDisconnect(port)) const tabId = port.sender?.tab?.id @@ -73,7 +73,7 @@ const onConnect = async function(port: chrome.runtime.Port) { await PageActionBackground.handleSidePanelConnect(port) } } -const onDisconnect = async function(port: chrome.runtime.Port) { +const onDisconnect = async function (port: chrome.runtime.Port) { if (port.name !== CONNECTION_APP) return if (chrome.runtime.lastError) { if ( @@ -170,24 +170,24 @@ const commandFuncs = { const cmd = isSearch ? { - id: params.id, - title: params.title, - searchUrl: params.searchUrl, - iconUrl: params.iconUrl, - openMode: params.openMode, - openModeSecondary: params.openModeSecondary, - spaceEncoding: params.spaceEncoding, - popupOption: PopupOption, - } - : isPageAction - ? { id: params.id, title: params.title, + searchUrl: params.searchUrl, iconUrl: params.iconUrl, openMode: params.openMode, - pageActionOption: params.pageActionOption, + openModeSecondary: params.openModeSecondary, + spaceEncoding: params.spaceEncoding, popupOption: PopupOption, } + : isPageAction + ? { + id: params.id, + title: params.title, + iconUrl: params.iconUrl, + openMode: params.openMode, + pageActionOption: params.pageActionOption, + popupOption: PopupOption, + } : null if (!cmd) { @@ -250,8 +250,8 @@ const commandFuncs = { } let targetId: number | undefined - const windowIdExists = await windowExists(w.srcWindowId) - if (windowIdExists) { + const { exists } = await windowExists(w.srcWindowId) + if (exists) { targetId = w.srcWindowId } else { const current = await chrome.windows.getCurrent() @@ -452,15 +452,9 @@ chrome.tabs.onActivated.addListener(async (activeInfo) => { console.error("Failed to close menu:", error) } - // Get the active tab's window and update screen ID try { - const tab = await chrome.tabs.get(activeInfo.tabId) - if (tab.windowId) { - await updateActiveScreenId(tab.windowId) - } - - // Update active tab ID - await updateActiveTabId(tab.id) + await updateActiveScreenId(activeInfo.windowId) + await updateActiveTabId(activeInfo.tabId) } catch (error) { console.error("Failed to get active screen ID:", error) } @@ -545,17 +539,17 @@ const checkAndPerformLegacyBackup = async () => { } } - // Initialize commandIdObj and register listener at top-level - // to ensure they are available when service worker restarts - ; (async () => { - try { - await ContextMenu.syncCommandIdObj() - chrome.contextMenus.onClicked.addListener(ContextMenu.onClicked) - } catch (error) { - // Ignore errors during initialization (e.g., in test environment) - console.debug("Failed to initialize context menu listener:", error) - } - })() +// Initialize commandIdObj and register listener at top-level +// to ensure they are available when service worker restarts +;(async () => { + try { + await ContextMenu.syncCommandIdObj() + chrome.contextMenus.onClicked.addListener(ContextMenu.onClicked) + } catch (error) { + // Ignore errors during initialization (e.g., in test environment) + console.debug("Failed to initialize context menu listener:", error) + } +})() Settings.addChangedListener(() => ContextMenu.init()) diff --git a/packages/extension/src/components/option/editor/ShortcutList.tsx b/packages/extension/src/components/option/editor/ShortcutList.tsx index 52117e8c..cfe6068c 100644 --- a/packages/extension/src/components/option/editor/ShortcutList.tsx +++ b/packages/extension/src/components/option/editor/ShortcutList.tsx @@ -1,5 +1,10 @@ import { useEffect, useState, useMemo } from "react" -import { Control, useFieldArray, useWatch } from "react-hook-form" +import { + Control, + useFieldArray, + useWatch, + useFormContext, +} from "react-hook-form" import { Keyboard, SquareArrowOutUpRight } from "lucide-react" import { SelectField } from "@/components/option/field/SelectField" import type { SelectOptionType } from "@/components/option/field/SelectField" @@ -7,6 +12,7 @@ import { t as _t } from "@/services/i18n" import { Ipc, BgCommand } from "@/services/ipc" import type { Command, CommandFolder, ShortcutCommand } from "@/types" import { + OPEN_MODE, OPEN_MODE_BG, SHORTCUT_PLACEHOLDER, SHORTCUT_NO_SELECTION_BEHAVIOR, @@ -18,6 +24,8 @@ import { } from "@/services/option/commandTree" import { cn } from "@/lib/utils" import css from "./ShortcutList.module.css" +import { isAiPromptType } from "@/types/schema" +import { InsertSymbol, INSERT } from "@/services/pageAction" const t = (key: string, p?: string[]) => _t(`Option_${key}`, p) @@ -25,24 +33,35 @@ type ShortcutListProps = { control: Control } -const isTextSelectionOnly = (openMode: string) => - !Object.values(OPEN_MODE_BG).includes(openMode as any) +const isTextSelectionOnly = (command: Command) => { + const { openMode } = command + if (isAiPromptType(command)) { + const option = command.aiPromptOption + + const willUseClipboard = + option.prompt.includes(InsertSymbol[INSERT.CLIPBOARD]) || + option.prompt.includes(InsertSymbol[INSERT.SELECTED_TEXT]) + return option.openMode === OPEN_MODE.SIDE_PANEL && willUseClipboard + } + + return !Object.values(OPEN_MODE_BG).includes(openMode as any) +} -const createNameRender = (openMode: string) => { - return isTextSelectionOnly(openMode) +const createNameRender = (command: Command) => { + return isTextSelectionOnly(command) ? (name: string) => ( - - {name} - - {t("shortcut_text_selection_only")} - + + {name} + + {t("shortcut_text_selection_only")} - ) + + ) : undefined } @@ -66,7 +85,7 @@ const flattenCommandsAndFolders = ( name: command.title, value: command.id, iconUrl: command.iconUrl, - nameRender: createNameRender(command.openMode), + nameRender: createNameRender(command), level: level, }) } else { @@ -90,6 +109,7 @@ const flattenCommandsAndFolders = ( export function ShortcutList({ control }: ShortcutListProps) { const [commands, setCommands] = useState([]) + const { setValue } = useFormContext() const { fields, replace } = useFieldArray<{ shortcuts: { shortcuts: ShortcutCommand[] } }>({ @@ -111,6 +131,26 @@ export function ShortcutList({ control }: ShortcutListProps) { name: "shortcuts.shortcuts", }) + useEffect(() => { + if (!shortcutValues) return + shortcutValues.forEach((shortcut: ShortcutCommand, index: number) => { + const cmd = userCommands.find( + (c: Command) => c?.id === shortcut?.commandId, + ) + if ( + cmd && + isTextSelectionOnly(cmd) && + shortcut?.noSelectionBehavior !== + SHORTCUT_NO_SELECTION_BEHAVIOR.DO_NOTHING + ) { + setValue( + `shortcuts.shortcuts.${index}.noSelectionBehavior`, + SHORTCUT_NO_SELECTION_BEHAVIOR.DO_NOTHING, + ) + } + }) + }, [shortcutValues, userCommands, setValue]) + const updateCommands = () => { chrome.commands.getAll((cmds) => { const filteredCommands = cmds.filter((cmd) => cmd.description !== "") @@ -210,8 +250,7 @@ export function ShortcutList({ control }: ShortcutListProps) { const selectedCmd = userCommands.find( (c: Command) => c?.id === targetId, ) - const showNoSel = - selectedCmd && !isTextSelectionOnly(selectedCmd.openMode) + const showNoSel = selectedCmd && !isTextSelectionOnly(selectedCmd) return (
diff --git a/packages/extension/src/const.ts b/packages/extension/src/const.ts index 6cd43bcf..e388b17d 100644 --- a/packages/extension/src/const.ts +++ b/packages/extension/src/const.ts @@ -136,7 +136,6 @@ export enum OPEN_MODE_BG { WINDOW = OPEN_MODE.WINDOW, TAB = OPEN_MODE.TAB, BACKGROUND_TAB = OPEN_MODE.BACKGROUND_TAB, - SIDE_PANEL = OPEN_MODE.SIDE_PANEL, API = OPEN_MODE.API, PAGE_ACTION = OPEN_MODE.PAGE_ACTION, AI_PROMPT = OPEN_MODE.AI_PROMPT, diff --git a/packages/extension/src/services/chrome.ts b/packages/extension/src/services/chrome.ts index 946fe324..e7dbba24 100644 --- a/packages/extension/src/services/chrome.ts +++ b/packages/extension/src/services/chrome.ts @@ -15,12 +15,16 @@ BgData.init() * @param {number} windowId - The ID of the window to check * @returns {Promise} True if the window exists, false otherwise */ -export const windowExists = async (windowId: number): Promise => { +export const windowExists = async ( + windowId: number, +): Promise< + { exists: true; window: chrome.windows.Window } | { exists: false } +> => { try { - await chrome.windows.get(windowId) - return true + const window = await chrome.windows.get(windowId) + return { exists: true, window } } catch { - return false + return { exists: false } } } diff --git a/packages/extension/src/services/screen.ts b/packages/extension/src/services/screen.ts index 395a3d38..5f942e1c 100644 --- a/packages/extension/src/services/screen.ts +++ b/packages/extension/src/services/screen.ts @@ -15,15 +15,14 @@ export type ScreenSize = { export async function updateActiveScreenId(windowId: number): Promise { try { - const exists = await windowExists(windowId) - if (!exists) { + const result = await windowExists(windowId) + if (!result.exists) { console.warn(`Window ${windowId} does not exist for screen ID update`) return } - const window = await chrome.windows.get(windowId) - const left = window.left ?? 0 - const top = window.top ?? 0 + const left = result.window.left ?? 0 + const top = result.window.top ?? 0 // Find the display that contains the window const displays = await chrome.system.display.getInfo() @@ -44,7 +43,7 @@ export async function updateActiveScreenId(windowId: number): Promise { })) } } catch (error) { - console.error("Failed to update active screen ID:", error) + console.warn("Failed to update active screen ID:", error) } } From ed9b8425dc48ebb87068739d71f7dbe1732a05dd Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Sat, 21 Mar 2026 16:13:35 +0900 Subject: [PATCH 2/2] Fix: Risk of false positives in symbol matching within the prompt. --- packages/extension/src/action/aiPrompt.ts | 4 +-- packages/extension/src/action/pageAction.ts | 4 +-- .../components/option/editor/ShortcutList.tsx | 30 +++++++++---------- .../services/option/defaultSettings.test.ts | 6 ++-- .../src/services/pageAction/index.ts | 4 +++ 5 files changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/extension/src/action/aiPrompt.ts b/packages/extension/src/action/aiPrompt.ts index 40bfa7f3..4dd4d7fb 100644 --- a/packages/extension/src/action/aiPrompt.ts +++ b/packages/extension/src/action/aiPrompt.ts @@ -14,7 +14,7 @@ import type { OpenAndRunProps } from "@/services/pageAction/background" import type { OpenSidePanelProps } from "@/services/chrome" import { findAiService } from "@/services/aiPrompt" import { isAiPromptType } from "@/types/schema" -import { InsertSymbol, INSERT } from "@/services/pageAction" +import { INSERT, toInsertTemplate } from "@/services/pageAction" import { Storage, SESSION_STORAGE_KEY } from "@/services/storage" // Map OPEN_MODE to PAGE_ACTION_OPEN_MODE for openAndRun @@ -108,7 +108,7 @@ export const AiPrompt = { // Checks if any step requires clipboard data const needClipboard = aiPromptOption.prompt.includes( - InsertSymbol[INSERT.CLIPBOARD], + toInsertTemplate(INSERT.CLIPBOARD), ) // Handle side panel mode: store pending steps in session storage, then open diff --git a/packages/extension/src/action/pageAction.ts b/packages/extension/src/action/pageAction.ts index cce58464..9cb1cefc 100644 --- a/packages/extension/src/action/pageAction.ts +++ b/packages/extension/src/action/pageAction.ts @@ -5,7 +5,7 @@ import { PAGE_ACTION_OPEN_MODE, PAGE_ACTION_EVENT } from "@/const" import { PopupOption } from "@/services/option/defaultSettings" import type { ExecuteCommandParams, UrlParam } from "@/types" import type { OpenAndRunProps } from "@/services/pageAction/background" -import { INSERT, InsertSymbol } from "@/services/pageAction" +import { INSERT, toInsertTemplate } from "@/services/pageAction" type PageActionParams = { userVariables?: Array<{ name: string; value: string }> @@ -38,7 +38,7 @@ export const PageAction = { const needClipboard = command.pageActionOption.steps.some((step) => { return ( step.param.type === PAGE_ACTION_EVENT.input && - step.param.value.includes(InsertSymbol[INSERT.CLIPBOARD]) + step.param.value.includes(toInsertTemplate(INSERT.CLIPBOARD)) ) }) diff --git a/packages/extension/src/components/option/editor/ShortcutList.tsx b/packages/extension/src/components/option/editor/ShortcutList.tsx index cfe6068c..438c8623 100644 --- a/packages/extension/src/components/option/editor/ShortcutList.tsx +++ b/packages/extension/src/components/option/editor/ShortcutList.tsx @@ -25,7 +25,7 @@ import { import { cn } from "@/lib/utils" import css from "./ShortcutList.module.css" import { isAiPromptType } from "@/types/schema" -import { InsertSymbol, INSERT } from "@/services/pageAction" +import { INSERT, toInsertTemplate } from "@/services/pageAction" const t = (key: string, p?: string[]) => _t(`Option_${key}`, p) @@ -39,8 +39,8 @@ const isTextSelectionOnly = (command: Command) => { const option = command.aiPromptOption const willUseClipboard = - option.prompt.includes(InsertSymbol[INSERT.CLIPBOARD]) || - option.prompt.includes(InsertSymbol[INSERT.SELECTED_TEXT]) + option.prompt.includes(toInsertTemplate(INSERT.CLIPBOARD)) || + option.prompt.includes(toInsertTemplate(INSERT.SELECTED_TEXT)) return option.openMode === OPEN_MODE.SIDE_PANEL && willUseClipboard } @@ -50,18 +50,18 @@ const isTextSelectionOnly = (command: Command) => { const createNameRender = (command: Command) => { return isTextSelectionOnly(command) ? (name: string) => ( - - {name} - - {t("shortcut_text_selection_only")} + + {name} + + {t("shortcut_text_selection_only")} + - - ) + ) : undefined } @@ -141,7 +141,7 @@ export function ShortcutList({ control }: ShortcutListProps) { cmd && isTextSelectionOnly(cmd) && shortcut?.noSelectionBehavior !== - SHORTCUT_NO_SELECTION_BEHAVIOR.DO_NOTHING + SHORTCUT_NO_SELECTION_BEHAVIOR.DO_NOTHING ) { setValue( `shortcuts.shortcuts.${index}.noSelectionBehavior`, diff --git a/packages/extension/src/services/option/defaultSettings.test.ts b/packages/extension/src/services/option/defaultSettings.test.ts index f6b2e042..87d6da28 100644 --- a/packages/extension/src/services/option/defaultSettings.test.ts +++ b/packages/extension/src/services/option/defaultSettings.test.ts @@ -1,10 +1,10 @@ import { describe, it, expect } from "vitest" import { DefaultCommands, getDefaultCommands } from "./defaultSettings" import { isLinkCommand } from "@/lib/utils" -import { INSERT, InsertSymbol } from "@/services/pageAction" +import { INSERT, toInsertTemplate } from "@/services/pageAction" -const SYM_SELECTED_TEXT = `{{${InsertSymbol[INSERT.SELECTED_TEXT]}}}` -const SYM_URL = `{{${InsertSymbol[INSERT.URL]}}}` +const SYM_SELECTED_TEXT = toInsertTemplate(INSERT.SELECTED_TEXT) +const SYM_URL = toInsertTemplate(INSERT.URL) const ALL_LOCALES = [ "ja", diff --git a/packages/extension/src/services/pageAction/index.ts b/packages/extension/src/services/pageAction/index.ts index 49cb131f..570db554 100644 --- a/packages/extension/src/services/pageAction/index.ts +++ b/packages/extension/src/services/pageAction/index.ts @@ -16,3 +16,7 @@ export const InsertSymbol = { [INSERT.URL]: "Url", [INSERT.CLIPBOARD]: "Clipboard", } + +/** Returns the template placeholder string for a given INSERT key, e.g. "{{Clipboard}}" */ +export const toInsertTemplate = (key: INSERT): string => + `{{${InsertSymbol[key]}}}`