diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e361b980..bd636bfb 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -35,6 +35,7 @@ "Bash(yarn tsc:*)", "Bash(yarn workspace @selection-command/extension:*)", "Bash(yarn workspace @selection-command/hub:*)", + "Bash(yarn workspace:*)", "WebFetch(domain:api.github.com)", "WebFetch(domain:developer.chrome.com)", "WebFetch(domain:docs.dndkit.com)", diff --git a/.serena/project.yml b/.serena/project.yml index 1e7df9da..9386ba4f 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -91,23 +91,15 @@ encoding: utf-8 # 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 # 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 fc3f0541..e1710e87 100644 --- a/packages/extension/public/_locales/en/messages.json +++ b/packages/extension/public/_locales/en/messages.json @@ -293,6 +293,9 @@ "Option_openMode_sidePanel": { "message": "Side Panel" }, + "Option_openMode_sidePanel_desc": { + "message": "Open in side panel." + }, "Option_openMode_api": { "message": "API" }, @@ -977,8 +980,11 @@ "Option_openMode_backgroundTab_desc": { "message": "Open tab in background.Show to the right of current tab." }, - "Option_openMode_sidePanel_desc": { - "message": "Open in side panel." + "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 69b3350f..d8717e87 100644 --- a/packages/extension/public/_locales/ja/messages.json +++ b/packages/extension/public/_locales/ja/messages.json @@ -329,6 +329,12 @@ "Option_openMode_backgroundTab_desc": { "message": "タブをバックグラウンドで開く。表示中タブの右側に表示する。" }, + "Option_openMode_currentTab": { + "message": "現在のタブ" + }, + "Option_openMode_currentTab_desc": { + "message": "現在アクティブなタブで実行する。URLが記録時の開始URLと一致する必要がある。" + }, "Option_openMode_sidePanel_desc": { "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 00000000..9654fe31 Binary files /dev/null and b/packages/extension/public/setting/open_mode/currentTab.png differ diff --git a/packages/extension/src/action/pageAction.ts b/packages/extension/src/action/pageAction.ts index e8f83795..d0c9c34f 100644 --- a/packages/extension/src/action/pageAction.ts +++ b/packages/extension/src/action/pageAction.ts @@ -53,7 +53,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 b67ef95f..87ce5e3e 100644 --- a/packages/extension/src/components/option/field/OpenModeToggleField.tsx +++ b/packages/extension/src/components/option/field/OpenModeToggleField.tsx @@ -37,6 +37,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" + } if (mode === OPEN_MODE.SIDE_PANEL) { return "/setting/open_mode/side_panel.png" } @@ -57,6 +60,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 = { @@ -95,7 +99,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 45e09f05..ae021771 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 { @@ -349,6 +350,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 5924a74f..597d724f 100644 --- a/packages/shared/src/constants/open-mode.ts +++ b/packages/shared/src/constants/open-mode.ts @@ -27,4 +27,5 @@ export enum PAGE_ACTION_OPEN_MODE { TAB = OPEN_MODE.TAB, BACKGROUND_TAB = OPEN_MODE.BACKGROUND_TAB, WINDOW = OPEN_MODE.WINDOW, + CURRENT_TAB = "currentTab", }