diff --git a/.github/workflows/upload.yaml b/.github/workflows/upload.yaml index 393a276..5da8a6b 100644 --- a/.github/workflows/upload.yaml +++ b/.github/workflows/upload.yaml @@ -50,22 +50,22 @@ jobs: git diff --exit-code -- calcit.cirru - name: Check Calcit types - run: calcit calcit.cirru --check-only + run: calcit calcit.cirru --compat-types --check-only - name: Check application dynamic methods - run: calcit calcit.cirru analyze dynamic-methods --max 0 --format json + run: calcit calcit.cirru --compat-types analyze dynamic-methods --max 0 --format json - name: Check deprecated Calcit APIs run: calcit calcit.cirru analyze deprecated --summary-only - name: Audit provider dynamic methods - run: calcit calcit.cirru analyze dynamic-methods --deps --format json + run: calcit calcit.cirru --compat-types analyze dynamic-methods --deps --format json - name: Check Calcit quality baseline run: calcit calcit.cirru analyze quality --baseline config/calcit-quality.cirru - name: Run Calcit regression tests - run: calcit calcit.cirru test --summary-only --format json + run: calcit calcit.cirru --compat-types test --summary-only --format json - name: Build extension package run: yarn build @@ -74,10 +74,14 @@ jobs: run: | test -f extension/dist/index.html find extension/dist/assets -maxdepth 1 -name '*.css' -print -quit | grep -q . - node --check extension/content.js + find extension/dist/assets -maxdepth 1 -name 'gemini-icon-*.png' -print -quit | grep -q . + find extension/dist/assets -maxdepth 1 -name 'axios-*.js' -print -quit | grep -q . node --check extension/service-worker.js node --check extension/get-selected.mjs - node -e 'const manifest = require("./extension/manifest.json"); if (manifest.manifest_version !== 3 || manifest.side_panel?.default_path !== "dist/index.html") process.exit(1)' + node -e 'const manifest = require("./extension/manifest.json"); if (manifest.manifest_version !== 3 || manifest.side_panel?.default_path !== "dist/index.html" || manifest.content_scripts || manifest.permissions.includes("tabs")) process.exit(1)' + test -z "$(grep -E 'https?://' extension/dist/index.html)" + test -z "$(grep -E 'axios-' extension/dist/index.html)" + test -z "$(grep -R 'feathericons.com' extension/dist/assets || true)" - name: Select deployment path if: github.event_name == 'push' && github.ref == 'refs/heads/main' diff --git a/calcit.cirru b/calcit.cirru index acb6a15..42deb26 100644 --- a/calcit.cirru +++ b/calcit.cirru @@ -3,7 +3,7 @@ :entries $ {} :default $ {} (:description |) (:init-fn 'app.main/main!) (:mode :js) (:reload-fn 'app.main/reload!) :feature-policy $ {} - :modules $ [] |respo.calcit/ |respo-ui.calcit/ |reel.calcit/ |respo-markdown.calcit/ |alerts.calcit/ |respo-feather.calcit/ + :modules $ [] |respo.calcit/ |respo-ui.calcit/ |reel.calcit/ |respo-markdown.calcit/ |alerts.calcit/ :type-slots $ {} :files $ {} 'app.comp.container $ %{} 'FileEntry @@ -83,6 +83,7 @@ :code $ quote defn call-anthropic-msg! (cursor state prompt-text model thinking? d!) hint-fn $ {} (:async true) + :args $ [] 'List 'app.schema/ChatState 'String 'String 'Bool 'Dynamic let abort $ deref *abort-control when (js-present-dynamic? abort) @@ -91,9 +92,12 @@ do (js/console.warn |Aborting-prev) (.!abort abort-controller) d! $ :: :change-model let + axios $ unsafe-coerce + .-default $ js-await (js/import |axios) + , 'Dynamic selected $ let selected0 $ js-await (get-selected) - if (js-present? selected0) (unsafe-coerce selected0 'String) "|<未找到内容>" + if (js-present? selected0) (stream-text selected0) "|<未找到内容>" content $ .replace prompt-text |{{selected}} selected messages0 $ append-user-message (:messages state) content messages1 $ upsert-assistant-message messages0 | | @@ -275,6 +279,7 @@ :code $ quote defn call-genai-msg! (variant cursor state prompt-text search? think? d! *text *thinking-text) hint-fn $ {} (:async true) + :args $ [] 'Tag 'List 'app.schema/ChatState 'String 'Bool 'Bool 'Dynamic 'Ref 'Ref if (= false @*gen-ai-new) let mod $ js-await (js/import |@google/genai) @@ -434,6 +439,7 @@ :code $ quote defn call-openrouter! (cursor state prompt-text variant thinking? d! *text) hint-fn $ {} (:async true) + :args $ [] 'List 'app.schema/ChatState 'String 'String 'Bool 'Dynamic 'Ref if (= false @*openai) let mod $ js-await (js/import |openai) @@ -801,7 +807,7 @@ :on-click $ fn (e d!) (.show sessions-plugin d!) &unit div {} $ :class-name style-history-button - comp-i |clock + comp-local-icon :clock 14 =< 4 nil if > (count sessions) 0 @@ -933,7 +939,7 @@ if dev? $ comp-inspect |Store app-store nil :examples $ [] :schema $ :: 'Fn - {} (:return 'Dynamic) + {} (:return 'respo.schema/Component) :args $ [] (:: 'Map 'Tag 'Dynamic) :features $ #{} :js-ffi 'comp-fill $ %{} 'CodeEntry (:doc |) @@ -945,9 +951,22 @@ when chrome-extension? $ js/chrome.runtime.sendMessage js-object (:action |fill-text) (:text text) , &unit - comp-i :send 12 :currentColor + comp-local-icon :send 12 :examples $ [] :schema $ :: 'Dynamic + 'comp-local-icon $ %{} 'CodeEntry (:doc |) + :code $ quote + defcomp comp-local-icon (icon size) + span $ {} (:aria-hidden |true) + :style $ {} (:display :inline-flex) (:align-items :center) (:justify-content :center) + :width $ str size |px + :height $ str size |px + :line-height |0 + :innerHTML $ case-default icon | (:clock "|") (:send "|") + :examples $ [] + :schema $ :: 'Fn + {} (:return 'respo.schema/Component) + :args $ [] 'Tag 'Number 'comp-message-box $ %{} 'CodeEntry (:doc |) :code $ quote defcomp comp-message-box (states picker-el on-submit model) @@ -1921,6 +1940,7 @@ :code $ quote defn submit-message! (cursor state prompt-text search? think? model d!) hint-fn $ {} (:async true) + :args $ [] 'List 'app.schema/ChatState 'String 'Bool 'Bool 'Tag 'Dynamic let state1 $ unsafe-coerce assoc state :messages $ append-user-message (:messages state) prompt-text @@ -2033,13 +2053,11 @@ respo.comp.inspect :refer $ comp-inspect reel.comp.reel :refer $ comp-reel app.config :refer $ dev? chrome-extension? site - |axios :default axios respo-md.comp.md :refer $ comp-md-block style-code-block respo-ui.comp :refer $ comp-copy style-close |../extension/get-selected :refer $ get-selected |../lib/db :refer $ db-get db-set |../lib/image :refer $ base64ToBlob - feather.core :refer $ comp-i respo-alerts.core :refer $ [] use-modal-menu use-prompt use-drawer use-alert respo-ui.util :refer $ tab-echo! app.schema :refer $ Store ChatState ChatSession ChatMessage MessageBoxState store @@ -2276,7 +2294,10 @@ , t_start , |ms :examples $ [] - :schema $ :: 'Dynamic + :schema $ :: 'Fn + {} (:return 'Unit) + :args $ [] + :features $ #{} :js-ffi 'sync-gemini-key! $ %{} 'CodeEntry (:doc |) :code $ quote defn sync-gemini-key! () $ when config/chrome-extension? diff --git a/deps.cirru b/deps.cirru index ad776cb..a17fb35 100644 --- a/deps.cirru +++ b/deps.cirru @@ -1,9 +1,8 @@ -{} (:calcit-version |0.13.77) +{} (:calcit-version |0.14.4) :version |0.0.6 :dependencies $ {} (|Respo/alerts.calcit |0.10.30) |Respo/reel.calcit |0.6.19 - |Respo/respo-feather.calcit |0.4.11 |Respo/respo-markdown.calcit |0.4.33 |Respo/respo-ui.calcit |0.7.19 |Respo/respo.calcit |0.16.95 diff --git a/extension/content.js b/extension/content.js deleted file mode 100644 index 8d4c506..0000000 --- a/extension/content.js +++ /dev/null @@ -1,61 +0,0 @@ -// Listen for messages -chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) { - // If the received message has the expected format... - console.log("[Side Message] msg", msg); - if (msg.get === "selected") { - // Call the specified callback, passing - // the web-page's DOM content as argument - sendResponse(getSelectedText()); - return; - } - if (msg.action === "fill-text") { - insertTextAtCursor(String(msg.text || "")); - } -}); - -let getSelectedText = () => { - if (window.getSelection) { - // 标准浏览器 - return window.getSelection().toString(); - } else if (document.selection) { - // IE 浏览器 - return document.selection.createRange().text; - } else { - return "<未获取到内容>"; - } -}; - -console.log("[Side Message] prepared content script"); - -function insertTextAtCursor(text) { - try { - const active = document.activeElement; - if (!active) return; - - if (active.tagName === "INPUT" || active.tagName === "TEXTAREA") { - const input = active; - if (input.readOnly || input.disabled) return; - const start = input.selectionStart ?? input.value.length; - const end = input.selectionEnd ?? input.value.length; - input.setRangeText(text, start, end, "end"); - input.dispatchEvent(new Event("input", { bubbles: true })); - return; - } - - if (active.isContentEditable) { - const sel = window.getSelection(); - if (!sel || sel.rangeCount === 0) return; - const range = sel.getRangeAt(0); - if (!active.contains(range.commonAncestorContainer)) return; - range.deleteContents(); - const textNode = document.createTextNode(text); - range.insertNode(textNode); - range.setStartAfter(textNode); - range.setEndAfter(textNode); - sel.removeAllRanges(); - sel.addRange(range); - } - } catch (err) { - console.error("[Side Message] failed to insert text", err); - } -} diff --git a/extension/get-selected.mjs b/extension/get-selected.mjs index d454c9f..28501a3 100644 --- a/extension/get-selected.mjs +++ b/extension/get-selected.mjs @@ -1,53 +1,24 @@ -export let get_selected = () => { - return new Promise((resolve, reject) => { - if (window.chrome?.runtime?.id == null) { - resolve(null); - return; - } - console.log("calling content script..."); - window.chrome.tabs - .query({ active: true, currentWindow: true }) - .then((x) => { - let activeTab = x[0]; - if (activeTab) { - let id = activeTab.id; - window.chrome.tabs.sendMessage(id, { get: "selected" }, function (response) { - // 接收来自 content.js 的返回数据 - // console.info('Content script returned: ' + response); - resolve(response); - }); - } else { - reject("found not active tab"); - } - }) - .catch((error) => { - console.error("Error", error); - }); - }); -}; - -// setTimeout(()=>{ -// chrome.tabs.query({active: true, currentWindow: true}).then(x => { -// let activeTab = x[0] -// if (activeTab) { -// let id = activeTab.id -// chrome.tabs.sendMessage(id, {get: 'selected'}, function(response) { -// // 接收来自 content.js 的返回数据 -// console.info('Content script returned: ' + response); -// }); +export let get_selected = async () => { + if (window.chrome?.runtime?.id == null) { + return null; + } -// } else { -// throw Error("no active tab found") -// } -// }) -// }, 2000) - -// chrome.scripting.executeScript({ -// target: { tabId: 1201634844 }, -// function: () => { console.log(document.body.innerText) } -// }); + try { + let [activeTab] = await window.chrome.tabs.query({ + active: true, + currentWindow: true, + }); + if (activeTab?.id == null) { + return null; + } -// chrome.tabs.sendMessage(1201634844, {get: 'selected'}, function(response) { -// // 接收来自 content.js 的返回数据 -// console.info('Content script returned: ' + response.message); -// }); + let results = await window.chrome.scripting.executeScript({ + target: { tabId: activeTab.id }, + func: () => window.getSelection?.().toString() ?? "", + }); + return results[0]?.result ?? ""; + } catch (error) { + console.warn("Unable to read selection from the active tab", error); + return null; + } +}; diff --git a/extension/manifest.json b/extension/manifest.json index 609389a..5eec083 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -12,13 +12,6 @@ "icons": { "128": "gemini-icon.png" }, - "content_scripts": [ - { - "matches": [""], - "match_origin_as_fallback": true, - "js": ["content.js"] - } - ], "side_panel": { "default_path": "dist/index.html" }, @@ -29,5 +22,5 @@ } } }, - "permissions": ["sidePanel", "scripting", "activeTab", "tabs", "contextMenus", "storage"] + "permissions": ["sidePanel", "scripting", "activeTab", "contextMenus", "storage"] } diff --git a/extension/service-worker.js b/extension/service-worker.js index 3f1c2a4..bdd0f43 100644 --- a/extension/service-worker.js +++ b/extension/service-worker.js @@ -74,14 +74,8 @@ chrome.contextMenus.onClicked.addListener((item, tab) => { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message && message.action === "fill-text") { - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - const tab = tabs && tabs[0]; - if (tab && tab.id != null) { - chrome.tabs.sendMessage(tab.id, { - action: "fill-text", - text: message.text || "", - }); - } + fillTextInActiveTab(message.text || "").catch((error) => { + console.warn("[Worker] Unable to fill text in the active tab:", error); }); } @@ -95,6 +89,47 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { } }); +async function fillTextInActiveTab(text) { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (tab?.id == null) { + return; + } + + await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: insertTextAtCursor, + args: [String(text)], + }); +} + +function insertTextAtCursor(text) { + const active = document.activeElement; + if (!active) return; + + if (active.tagName === "INPUT" || active.tagName === "TEXTAREA") { + if (active.readOnly || active.disabled) return; + const start = active.selectionStart ?? active.value.length; + const end = active.selectionEnd ?? active.value.length; + active.setRangeText(text, start, end, "end"); + active.dispatchEvent(new Event("input", { bubbles: true })); + return; + } + + if (active.isContentEditable) { + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) return; + const range = selection.getRangeAt(0); + if (!active.contains(range.commonAncestorContainer)) return; + range.deleteContents(); + const textNode = document.createTextNode(text); + range.insertNode(textNode); + range.setStartAfter(textNode); + range.setEndAfter(textNode); + selection.removeAllRanges(); + selection.addRange(range); + } +} + // ========================================================================= // Page Translation // ========================================================================= diff --git a/history/202609091100-optimize-extension-startup.md b/history/202609091100-optimize-extension-startup.md new file mode 100644 index 0000000..cfa3df3 --- /dev/null +++ b/history/202609091100-optimize-extension-startup.md @@ -0,0 +1,15 @@ +# Optimize extension startup / 优化扩展启动 + +- Minify production bundles with Vite's Oxc minifier to reduce side-panel parsing work. +- Replace the remote favicon and unused web-app manifest request with the packaged extension icon. +- Replace the `` startup content script with on-demand `chrome.scripting.executeScript` calls authorized by the existing `activeTab` permission. +- Resolve selection failures to `null` so restricted pages cannot leave side-panel initialization waiting on an unsettled Promise. +- Upgrade the project toolchain to Calcit and `@calcit/procs` 0.14.4, add explicit async-boundary type hints, and use the compiler's compatibility mode for the remaining Respo `Dynamic` boundaries so `yarn build` works without a local downgrade. +- Replace the full Feather icon dependency with two local inline SVGs, and dynamically import Axios only when the Anthropic provider is used. + +- 使用 Vite 的 Oxc 压缩生产构建,减少侧边栏启动时的解析工作。 +- 使用扩展包内图标替代远程 favicon,并移除未使用的 Web App Manifest 请求。 +- 移除在 `` 页面启动时注入的 content script,改由现有 `activeTab` 权限授权,在读取选区及填写文本时按需执行 `chrome.scripting.executeScript`。 +- 受限页面无法读取选区时返回 `null`,避免未结束的 Promise 让侧边栏初始化持续等待。 +- 将项目工具链升级到 Calcit 与 `@calcit/procs` 0.14.4,为异步边界补充显式类型提示,并对仍由 Respo 使用的 `Dynamic` 边界启用编译器兼容模式,使 `yarn build` 无需本地降级即可通过。 +- 使用两个本地内联 SVG 替代完整 Feather 图标依赖,并仅在调用 Anthropic 服务时动态加载 Axios。 diff --git a/index.html b/index.html index 063b0d6..83dd5d4 100644 --- a/index.html +++ b/index.html @@ -2,12 +2,7 @@ Gemini Msg - - +