Skip to content
Merged
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
4 changes: 2 additions & 2 deletions packages/extension/src/action/aiPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions packages/extension/src/action/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/extension/src/action/pageAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>
Expand Down Expand Up @@ -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))
)
})

Expand Down
64 changes: 29 additions & 35 deletions packages/extension/src/background_script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 (
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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())

Expand Down
55 changes: 47 additions & 8 deletions packages/extension/src/components/option/editor/ShortcutList.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
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"
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,
Expand All @@ -18,18 +24,31 @@ import {
} from "@/services/option/commandTree"
import { cn } from "@/lib/utils"
import css from "./ShortcutList.module.css"
import { isAiPromptType } from "@/types/schema"
import { INSERT, toInsertTemplate } from "@/services/pageAction"

const t = (key: string, p?: string[]) => _t(`Option_${key}`, p)

type ShortcutListProps = {
control: Control<any>
}

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(toInsertTemplate(INSERT.CLIPBOARD)) ||
option.prompt.includes(toInsertTemplate(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) => (
<span className="truncate">
{name}
Expand Down Expand Up @@ -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 {
Expand All @@ -90,6 +109,7 @@ const flattenCommandsAndFolders = (

export function ShortcutList({ control }: ShortcutListProps) {
const [commands, setCommands] = useState<chrome.commands.Command[]>([])
const { setValue } = useFormContext()
const { fields, replace } = useFieldArray<{
shortcuts: { shortcuts: ShortcutCommand[] }
}>({
Expand All @@ -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 !== "")
Expand Down Expand Up @@ -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 (
<div key={field.id} className="space-y-2">
Expand Down
1 change: 0 additions & 1 deletion packages/extension/src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 8 additions & 4 deletions packages/extension/src/services/chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ BgData.init()
* @param {number} windowId - The ID of the window to check
* @returns {Promise<boolean>} True if the window exists, false otherwise
*/
export const windowExists = async (windowId: number): Promise<boolean> => {
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 }
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/extension/src/services/pageAction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]}}}`
11 changes: 5 additions & 6 deletions packages/extension/src/services/screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@ export type ScreenSize = {

export async function updateActiveScreenId(windowId: number): Promise<void> {
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()
Expand All @@ -44,7 +43,7 @@ export async function updateActiveScreenId(windowId: number): Promise<void> {
}))
}
} catch (error) {
console.error("Failed to update active screen ID:", error)
console.warn("Failed to update active screen ID:", error)
}
}

Expand Down
Loading