From 25ee6286bc2f67d537ee9678eac2131cf3cd8a41 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Thu, 19 Feb 2026 21:26:21 +0900 Subject: [PATCH] Add: Enable PageAction to run in the current tab. --- .claude/settings.local.json | 3 +- .serena/project.yml | 61 +++++++++++++-- .../public/_locales/en/messages.json | 6 ++ .../public/_locales/ja/messages.json | 6 ++ .../public/setting/open_mode/currentTab.png | Bin 0 -> 756 bytes packages/extension/src/action/pageAction.ts | 5 +- .../option/field/OpenModeToggleField.tsx | 9 ++- packages/extension/src/lib/utils.test.ts | 73 +++++++++++++++++- packages/extension/src/lib/utils.ts | 20 +++++ .../src/services/pageAction/background.ts | 24 ++++++ packages/shared/src/constants/open-mode.ts | 1 + 11 files changed, 196 insertions(+), 12 deletions(-) create mode 100644 packages/extension/public/setting/open_mode/currentTab.png diff --git a/.claude/settings.local.json b/.claude/settings.local.json index fbe5e4c3..d5a8cef7 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -56,7 +56,8 @@ "mcp__serena__think_about_whether_you_are_done", "mcp__serena__write_memory", "Bash(yarn build:*)", - "mcp__serena__initial_instructions" + "mcp__serena__initial_instructions", + "Bash(yarn workspace:*)" ], "deny": [] } diff --git a/.serena/project.yml b/.serena/project.yml index 53d89295..49cb7746 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -1,10 +1,3 @@ -# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby) -# * For C, use cpp -# * For JavaScript, use typescript -# Special requirements: -# * csharp: Requires the presence of a .sln file in the project folder. -language: typescript - # whether to use the project's gitignore file to ignore files # Added on 2025-04-07 ignore_all_files_in_gitignore: true @@ -63,5 +56,57 @@ excluded_tools: [] # initial prompt for the project. It will always be given to the LLM upon activating the project # (contrary to the memories, which are loaded on demand). initial_prompt: "" - +# the name by which the project can be referenced within Serena project_name: "selection-command" + +# list of mode names to that are always to be included in the set of active modes +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this setting overrides the global configuration. +# Set this to [] to disable base modes for this project. +# Set this to a list of mode names to always include the respective modes for this project. +base_modes: + +# list of mode names that are to be activated by default. +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# This setting can, in turn, be overridden by CLI parameters (--mode). +default_modes: + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) +included_optional_tools: [] + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +fixed_tools: [] + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: utf-8 + +# list of languages for which language servers are started; choose from: +# al bash clojure cpp csharp +# csharp_omnisharp dart elixir elm erlang +# fortran fsharp go groovy haskell +# java julia kotlin lua markdown +# matlab nix pascal perl php +# php_phpactor powershell python python_jedi r +# rego ruby ruby_solargraph rust scala +# swift terraform toml typescript typescript_vts +# vue yaml zig +# (This list may be outdated. For the current list, see values of Language enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py +# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# - For Free Pascal/Lazarus, use pascal +# Special requirements: +# Some languages require additional setup/installations. +# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers +# When using multiple languages, the first language server that supports a given file will be used for that file. +# The first language is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +languages: + - typescript diff --git a/packages/extension/public/_locales/en/messages.json b/packages/extension/public/_locales/en/messages.json index 077388d3..d5d2459d 100644 --- a/packages/extension/public/_locales/en/messages.json +++ b/packages/extension/public/_locales/en/messages.json @@ -953,6 +953,12 @@ "Option_openMode_backgroundTab_desc": { "message": "Open tab in background.Show to the right of current tab." }, + "Option_openMode_currentTab": { + "message": "Current Tab" + }, + "Option_openMode_currentTab_desc": { + "message": "Run on the current active tab.URL must match the recorded start URL." + }, "Option_title_desc": { "message": "Displayed as the command title." }, diff --git a/packages/extension/public/_locales/ja/messages.json b/packages/extension/public/_locales/ja/messages.json index 5d956f30..299c66f8 100644 --- a/packages/extension/public/_locales/ja/messages.json +++ b/packages/extension/public/_locales/ja/messages.json @@ -317,6 +317,12 @@ "Option_openMode_backgroundTab_desc": { "message": "タブをバックグラウンドで開く。表示中タブの右側に表示する。" }, + "Option_openMode_currentTab": { + "message": "現在のタブ" + }, + "Option_openMode_currentTab_desc": { + "message": "現在アクティブなタブで実行する。URLが記録時の開始URLと一致する必要がある。" + }, "Option_commandType_title": { "message": "コマンドの種類を選ぶ" }, diff --git a/packages/extension/public/setting/open_mode/currentTab.png b/packages/extension/public/setting/open_mode/currentTab.png new file mode 100644 index 0000000000000000000000000000000000000000..9654fe31d57e9c7d87317414c686244e61baf8b5 GIT binary patch literal 756 zcmeAS@N?(olHy`uVBq!ia0vp^J|N7&1|*M957Y)yoCO|{#S9E$svykh8Km+7D9BhG zO^KUtbuzyIO!f9BNkl+-O zl(1#S#0)z}qh-;v>KcqTiK z`#!b*^IO*3kcj_RS2u@O5NI15G&rw!*&BC%MLmD}VUPdSZ3j4*j9U)y$iBV(q3`3P zl+8EieA?vl^3#*27Jk(R5 zqO#+@+HcuQUz#_ZYks`NZE;|9z5k0zQ#SnQwa?}CK5-+Z$0*FJqh^{^Siu~z_4N~+ zH4Ikj#O+A2HeXeEk%)8h@P&4tR>m&C0Z(sjjAu48-EEKl;>LRbDld>WfMXk*{u9SRf zP7qJb()lM8bT)GLOy+z0Z%sDTJ-Dh+Fym`?q*$9^_NwH5{0f!KkYs1{PUF63IEj04UYG>esfv+PHk7`9dEDH zn=;dszWvcHYxSIeN&m3ZLWi?ylTST0dRk=Zb-Kc4S>ATluaPhPC${@g z-p*YiLSgH#D_<`S=Jmdkwe@X8iqEVQpSSPzwO{48e6#EdUz_ub-IPnBUljXSc?+ui zvpKVEL(3L6-%h0}QGS8RIlL8*vNFEwN`4KWvZU(%e3!jMIM@*`2>~Y+raF8^rf?^>bP0l+XkKzD!K! literal 0 HcmV?d00001 diff --git a/packages/extension/src/action/pageAction.ts b/packages/extension/src/action/pageAction.ts index 63339347..d99b60be 100644 --- a/packages/extension/src/action/pageAction.ts +++ b/packages/extension/src/action/pageAction.ts @@ -44,7 +44,10 @@ export const PageAction = { ? PAGE_ACTION_OPEN_MODE.WINDOW : command.pageActionOption.openMode === PAGE_ACTION_OPEN_MODE.WINDOW ? PAGE_ACTION_OPEN_MODE.TAB - : PAGE_ACTION_OPEN_MODE.TAB + : command.pageActionOption.openMode === + PAGE_ACTION_OPEN_MODE.CURRENT_TAB + ? PAGE_ACTION_OPEN_MODE.TAB // Open in new tab when secondary is pressed + : PAGE_ACTION_OPEN_MODE.TAB : command.pageActionOption.openMode const windowPosition = await getWindowPosition() diff --git a/packages/extension/src/components/option/field/OpenModeToggleField.tsx b/packages/extension/src/components/option/field/OpenModeToggleField.tsx index c462dd43..b607f86e 100644 --- a/packages/extension/src/components/option/field/OpenModeToggleField.tsx +++ b/packages/extension/src/components/option/field/OpenModeToggleField.tsx @@ -32,6 +32,9 @@ const getIconForMode = (mode: string) => { if (mode === OPEN_MODE.WINDOW || mode === PAGE_ACTION_OPEN_MODE.WINDOW) { return "/setting/open_mode/window.png" } + if (mode === PAGE_ACTION_OPEN_MODE.CURRENT_TAB) { + return "/setting/open_mode/currentTab.png" + } return "/setting/open_mode/popup.png" } @@ -48,6 +51,7 @@ const PAGE_ACTION_MODES = [ PAGE_ACTION_OPEN_MODE.WINDOW, PAGE_ACTION_OPEN_MODE.TAB, PAGE_ACTION_OPEN_MODE.BACKGROUND_TAB, + PAGE_ACTION_OPEN_MODE.CURRENT_TAB, ] as const type OpenModeToggleFieldProps = { @@ -86,7 +90,10 @@ export const OpenModeToggleField = ({ onValueChange={(val) => { if (val) field.onChange(val) }} - className="grid grid-cols-4 gap-2 py-1" + className={cn( + "grid gap-2 py-1", + type === "pageAction" ? "grid-cols-5" : "grid-cols-4", + )} > {modes.map((mode) => { const iconSrc = getIconForMode(mode) diff --git a/packages/extension/src/lib/utils.test.ts b/packages/extension/src/lib/utils.test.ts index effb6451..7b691870 100644 --- a/packages/extension/src/lib/utils.test.ts +++ b/packages/extension/src/lib/utils.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest" -import { parseGeminiUrl, toUrl } from "./utils" +import { parseGeminiUrl, toUrl, matchesPageActionUrl } from "./utils" import { SPACE_ENCODING } from "@/const" import type { UrlParam } from "@/types" @@ -180,3 +180,74 @@ describe("toUrl", () => { ) }) }) + +describe("matchesPageActionUrl", () => { + it("MU-01: exact match without wildcard", () => { + expect( + matchesPageActionUrl( + "https://example.com/page", + "https://example.com/page", + ), + ).toBe(true) + }) + + it("MU-02: no match without wildcard - different domain", () => { + expect( + matchesPageActionUrl( + "https://example.com/page", + "https://other.com/page", + ), + ).toBe(false) + }) + + it("MU-03: no match without wildcard - different path", () => { + expect( + matchesPageActionUrl( + "https://example.com/page", + "https://example.com/other", + ), + ).toBe(false) + }) + + it("MU-04: wildcard matches any path suffix", () => { + expect( + matchesPageActionUrl( + "https://example.com/*", + "https://example.com/path?q=1", + ), + ).toBe(true) + }) + + it("MU-05: wildcard at end of path matches query string", () => { + expect( + matchesPageActionUrl( + "https://example.com/search*", + "https://example.com/search?q=foo", + ), + ).toBe(true) + }) + + it("MU-06: wildcard in hostname matches subdomain", () => { + expect( + matchesPageActionUrl( + "https://*.example.com/page", + "https://sub.example.com/page", + ), + ).toBe(true) + }) + + it("MU-07: wildcard does not match different domain", () => { + expect( + matchesPageActionUrl("https://example.com/*", "https://other.com/path"), + ).toBe(false) + }) + + it("MU-08: wildcard matches empty suffix", () => { + expect( + matchesPageActionUrl( + "https://example.com/path*", + "https://example.com/path", + ), + ).toBe(true) + }) +}) diff --git a/packages/extension/src/lib/utils.ts b/packages/extension/src/lib/utils.ts index 00071d64..fa72918b 100644 --- a/packages/extension/src/lib/utils.ts +++ b/packages/extension/src/lib/utils.ts @@ -390,6 +390,26 @@ export function validateUserVariables(variables: UserVariable[]): boolean { ) } +/** + * Checks whether a URL matches a pattern with wildcard (*) support. + * `*` in the pattern matches any sequence of characters (including empty). + * + * Example: + * matchesPageActionUrl("https://example.com/*", "https://example.com/path?q=1") → true + * matchesPageActionUrl("https://example.com/page", "https://example.com/page") → true + * matchesPageActionUrl("https://example.com/page", "https://other.com/page") → false + */ +export function matchesPageActionUrl(pattern: string, url: string): boolean { + if (!pattern.includes("*")) { + return pattern === url + } + // Escape regex special chars except *, then replace * with .* + const regexStr = pattern + .replace(/[.+?^${}()|[\]\\]/g, "\\$&") + .replace(/\*/g, ".*") + return new RegExp(`^${regexStr}$`).test(url) +} + /** * Parse markdown URL format in Gemini and extract the actual URL. * @param {string} text - Text that might contain markdown URL diff --git a/packages/extension/src/services/pageAction/background.ts b/packages/extension/src/services/pageAction/background.ts index 6ca1364e..f59c97fd 100644 --- a/packages/extension/src/services/pageAction/background.ts +++ b/packages/extension/src/services/pageAction/background.ts @@ -30,6 +30,7 @@ import { isEmpty, isUrl, isUrlParam, + matchesPageActionUrl, sleep, } from "@/lib/utils" import type { @@ -339,6 +340,29 @@ export const openAndRun = ( }) tabId = ret.tabId clipboardText = ret.clipboardText + } else if (param.openMode === PAGE_ACTION_OPEN_MODE.CURRENT_TAB) { + // Execute on the current active tab without opening a new tab/window + const currentTab = await getCurrentTab() + if (!currentTab?.id) { + console.error("No active tab found") + response(false) + return + } + const startUrl = isUrlParam(param.url) ? null : param.url + if ( + startUrl && + currentTab.url && + !matchesPageActionUrl(startUrl, currentTab.url) + ) { + console.warn("Current tab URL does not match the recorded start URL", { + startUrl, + currentUrl: currentTab.url, + }) + response(false) + return + } + tabId = currentTab.id + clipboardText = "" } else { // Popup and Window modes const ret = await openPopupWindow({ diff --git a/packages/shared/src/constants/open-mode.ts b/packages/shared/src/constants/open-mode.ts index 81d0b89a..91b826da 100644 --- a/packages/shared/src/constants/open-mode.ts +++ b/packages/shared/src/constants/open-mode.ts @@ -26,4 +26,5 @@ export enum PAGE_ACTION_OPEN_MODE { TAB = OPEN_MODE.TAB, BACKGROUND_TAB = OPEN_MODE.BACKGROUND_TAB, WINDOW = OPEN_MODE.WINDOW, + CURRENT_TAB = "currentTab", }