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
-
-
+