diff --git a/packages/extension/src/background_script.ts b/packages/extension/src/background_script.ts index a5fb8664..736f6034 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, - openModeSecondary: params.openModeSecondary, - spaceEncoding: params.spaceEncoding, + pageActionOption: params.pageActionOption, popupOption: PopupOption, } - : isPageAction - ? { - id: params.id, - title: params.title, - iconUrl: params.iconUrl, - openMode: params.openMode, - pageActionOption: params.pageActionOption, - popupOption: PopupOption, - } : null if (!cmd) { @@ -229,6 +229,7 @@ const commandFuncs = { ) => { const handleOpenInTab = async () => { let w: WindowType | undefined + const targetUrl = sender.tab?.url ?? sender.url const stack = await WindowStackManager.getStack() for (const layer of stack) { @@ -241,7 +242,7 @@ const commandFuncs = { } if (!w || w.srcWindowId == null) { console.warn("window not found", sender.tab?.windowId) - chrome.tabs.create({ url: sender.url }) + chrome.tabs.create({ url: targetUrl }) await closeWindow(sender.tab?.windowId as number, "openInTab") await WindowStackManager.removeWindow(sender.tab?.windowId as number) response(true) @@ -261,10 +262,7 @@ const commandFuncs = { } if (targetId) { - chrome.tabs.create({ - url: sender.url, - windowId: targetId, - }) + chrome.tabs.create({ url: targetUrl, windowId: targetId }) await closeWindow(sender.tab?.windowId as number, "openInTab") await WindowStackManager.removeWindow(sender.tab?.windowId as number) response(true) @@ -547,17 +545,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/const.ts b/packages/extension/src/const.ts index d3738951..6cd43bcf 100644 --- a/packages/extension/src/const.ts +++ b/packages/extension/src/const.ts @@ -109,8 +109,8 @@ export const COMMAND_TYPE_GROUPS = [ titleKey: "commandGroup_webPage_title", types: [ COMMAND_TYPE.SEARCH, - COMMAND_TYPE.PAGE_ACTION, COMMAND_TYPE.AI_PROMPT, + COMMAND_TYPE.PAGE_ACTION, ], }, { diff --git a/packages/extension/src/services/aiPrompt.ts b/packages/extension/src/services/aiPrompt.ts index 48f9ccbd..48728282 100644 --- a/packages/extension/src/services/aiPrompt.ts +++ b/packages/extension/src/services/aiPrompt.ts @@ -19,76 +19,13 @@ export type AiService = { /** External endpoint URL for AI service config data. */ const AI_SERVICES_URL = `${HUB_URL}/data/ai-services.json` -/** - * List of supported AI services with their DOM selectors. - * Used as fallback when the external fetch fails and no cache is available. - * Selector arrays are tried in order, using the first one that matches. - */ -const AI_SERVICES_FALLBACK: AiService[] = [ - { - id: "chatgpt", - name: "ChatGPT", - url: "https://chatgpt.com", - faviconUrl: "https://chatgpt.com/favicon.ico", - inputSelectors: ["#prompt-textarea", "[data-testid='prompt-textarea']"], - submitSelectors: [ - "form button.composer-submit-button-color", - "button#composer-submit-button", - "button[data-testid='composer-speech-button']", - ], - selectorType: SelectorType.css, - }, - { - id: "gemini", - name: "Gemini", - url: "https://gemini.google.com/app", - faviconUrl: - "https://www.gstatic.com/lamda/images/gemini_sparkle_aurora_33f86dc0c0257da337c63.svg", - inputSelectors: [".ql-editor[contenteditable='true']"], - submitSelectors: ["button.send-button", "button mat-icon[fonticon='send']"], - selectorType: SelectorType.css, - }, - { - id: "claude", - name: "Claude", - url: "https://claude.ai/new", - faviconUrl: "https://claude.ai/favicon.ico", - inputSelectors: [ - "div[contenteditable='true'][aria-label]", - "div[contenteditable='true'].ProseMirror", - ], - submitSelectors: [ - "#main-content button.Button_claude__c_hZy", - "#main-content button[data-state='closed']", - ], - selectorType: SelectorType.css, - }, - { - id: "perplexity", - name: "Perplexity", - url: "https://www.perplexity.ai", - faviconUrl: "https://favicon.im/perplexity.ai", - inputSelectors: [ - "div#ask-input", - "div[contenteditable='true'][role='textbox']", - ], - submitSelectors: [ - "button[type='button']:has(use[*|href='#pplx-icon-custom-perplexity-v2v'])", - "button[type='button']:has(use[*|href='#pplx-icon-arrow-up'])", - "button[type='button']:has(use[*|href^='#pplx-icon-arrow-right'])", - "button[aria-label='Submit']", - ], - selectorType: SelectorType.css, - }, -] - /** Today's date string "YYYY-MM-DD" used as cache TTL key. */ const todayStr = (): string => new Date().toISOString().slice(0, 10) /** * Normalize raw JSON data fetched from the external endpoint into AiService[]. - * Items that are missing required fields (id, url, inputSelectors, submitSelectors) - * are silently skipped. The external JSON may omit `selectorType`, defaulting to CSS. + * - Items that are missing required fields (id, url, inputSelectors, submitSelectors) are silently skipped. + * - The external JSON may omit `selectorType`, defaulting to `CSS`. */ const normalizeServices = (raw: unknown[]): AiService[] => { const results: AiService[] = [] @@ -118,6 +55,15 @@ const normalizeServices = (raw: unknown[]): AiService[] => { return results } +/** + * List of supported AI services with their DOM selectors. + * Built from the hub's ai-services.json at compile time. + * Used as fallback when the external fetch fails and no cache is available. + * Selector arrays are tried in order, using the first one that matches. + */ +const AI_SERVICES_FALLBACK: AiService[] = + normalizeServices(__AI_SERVICES_JSON__) + /** * Retrieve AI service definitions. * Strategy: diff --git a/packages/extension/src/types/vite-env.d.ts b/packages/extension/src/types/vite-env.d.ts index c5829f78..6664dba0 100644 --- a/packages/extension/src/types/vite-env.d.ts +++ b/packages/extension/src/types/vite-env.d.ts @@ -1,7 +1,7 @@ /// // eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface ImportMetaEnv {} +interface ImportMetaEnv { } interface ImportMeta { readonly env: ImportMetaEnv @@ -9,3 +9,4 @@ interface ImportMeta { declare const __APP_VERSION__: string declare const __APP_NAME__: string +declare const __AI_SERVICES_JSON__: unknown[] diff --git a/packages/extension/vite.config.ts b/packages/extension/vite.config.ts index bffe9acd..3dc1bfe9 100644 --- a/packages/extension/vite.config.ts +++ b/packages/extension/vite.config.ts @@ -1,3 +1,4 @@ +import fs from "fs" import path from "path" import { defineConfig, loadEnv } from "vite" import react from "@vitejs/plugin-react" @@ -106,6 +107,10 @@ export default defineConfig(({ mode }) => { define: { __APP_NAME__: JSON.stringify(packageJson.name), __APP_VERSION__: JSON.stringify(packageJson.version), + __AI_SERVICES_JSON__: fs.readFileSync( + path.resolve(__dirname, "../hub/public/data/ai-services.json"), + "utf-8", + ), }, resolve: { alias: { @@ -118,20 +123,20 @@ export default defineConfig(({ mode }) => { pure: mode === "production" ? [ - "console.log", - "console.debug", - "console.info", - "console.trace", - "console.dir", - "console.count", - "console.countReset", - "console.group", - "console.groupCollapsed", - "console.groupEnd", - "console.time", - "console.timeEnd", - "console.timeLog", - ] + "console.log", + "console.debug", + "console.info", + "console.trace", + "console.dir", + "console.count", + "console.countReset", + "console.group", + "console.groupCollapsed", + "console.groupEnd", + "console.time", + "console.timeEnd", + "console.timeLog", + ] : [], }, build: { diff --git a/packages/extension/vitest.config.ts b/packages/extension/vitest.config.ts index ce33c459..f1672d44 100644 --- a/packages/extension/vitest.config.ts +++ b/packages/extension/vitest.config.ts @@ -1,3 +1,4 @@ +import fs from "fs" import { defineConfig, mergeConfig } from "vitest/config" import { resolve } from "path" import packageJson from "./package.json" @@ -13,6 +14,10 @@ export default mergeConfig( define: { __APP_NAME__: JSON.stringify(packageJson.name), __APP_VERSION__: JSON.stringify(packageJson.version), + __AI_SERVICES_JSON__: fs.readFileSync( + resolve(__dirname, "../hub/public/data/ai-services.json"), + "utf-8", + ), }, resolve: { alias: { diff --git a/packages/hub/public/data/ai-services.json b/packages/hub/public/data/ai-services.json index 2783b5d3..0a280905 100644 --- a/packages/hub/public/data/ai-services.json +++ b/packages/hub/public/data/ai-services.json @@ -4,10 +4,7 @@ "name": "ChatGPT", "url": "https://chatgpt.com", "faviconUrl": "https://chatgpt.com/favicon.ico", - "inputSelectors": [ - "#prompt-textarea", - "[data-testid='prompt-textarea']" - ], + "inputSelectors": ["#prompt-textarea", "[data-testid='prompt-textarea']"], "submitSelectors": [ "form button.composer-submit-button-color", "button#composer-submit-button", @@ -19,9 +16,7 @@ "name": "Gemini", "url": "https://gemini.google.com/app", "faviconUrl": "https://www.gstatic.com/lamda/images/gemini_sparkle_aurora_33f86dc0c0257da337c63.svg", - "inputSelectors": [ - ".ql-editor[contenteditable='true']" - ], + "inputSelectors": [".ql-editor[contenteditable='true']"], "submitSelectors": [ "button.send-button", "button mat-icon[fonticon='send']" @@ -31,7 +26,7 @@ "id": "claude", "name": "Claude", "url": "https://claude.ai/new", - "faviconUrl": "https://claude.ai/favicon.ico", + "faviconUrl": "https://favicon.im/claude.ai", "inputSelectors": [ "div[contenteditable='true'][aria-label]", "div[contenteditable='true'].ProseMirror"