Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
18 changes: 5 additions & 13 deletions .serena/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 8 additions & 2 deletions packages/extension/public/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,9 @@
"Option_openMode_sidePanel": {
"message": "Side Panel"
},
"Option_openMode_sidePanel_desc": {
"message": "Open in side panel."
},
"Option_openMode_api": {
"message": "API"
},
Expand Down Expand Up @@ -977,8 +980,11 @@
"Option_openMode_backgroundTab_desc": {
"message": "Open tab in background.<wbr>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.<wbr>URL must match the recorded start URL."
},
"Option_title_desc": {
"message": "Displayed as the command title."
Expand Down
6 changes: 6 additions & 0 deletions packages/extension/public/_locales/ja/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,12 @@
"Option_openMode_backgroundTab_desc": {
"message": "タブをバックグラウンドで開く。<wbr>表示中タブの右側に表示する。"
},
"Option_openMode_currentTab": {
"message": "現在のタブ"
},
"Option_openMode_currentTab_desc": {
"message": "現在アクティブなタブで実行する。<wbr>URLが記録時の開始URLと一致する必要がある。"
},
"Option_openMode_sidePanel_desc": {
"message": "サイドパネルで開く。"
},
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion packages/extension/src/action/pageAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand All @@ -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 = {
Expand Down Expand Up @@ -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)
Expand Down
73 changes: 72 additions & 1 deletion packages/extension/src/lib/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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)
})
})
20 changes: 20 additions & 0 deletions packages/extension/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions packages/extension/src/services/pageAction/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
isEmpty,
isUrl,
isUrlParam,
matchesPageActionUrl,
sleep,
} from "@/lib/utils"
import type {
Expand Down Expand Up @@ -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({
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/constants/open-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
Loading