Skip to content

Commit 4940cdf

Browse files
committed
better message interop of extension
1 parent 63151b4 commit 4940cdf

File tree

6 files changed

+165
-19
lines changed

6 files changed

+165
-19
lines changed

compact.cirru

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
{} (:package |app)
2+
{} (:about "|file is generated - never edit directly; learn cr edit/tree workflows before changing") (:package |app)
33
:configs $ {} (:init-fn |app.main/main!) (:reload-fn |app.main/reload!) (:version |0.0.1)
44
:modules $ [] |respo.calcit/ |lilac/ |memof/ |respo-ui.calcit/ |reel.calcit/ |respo-markdown.calcit/ |alerts.calcit/
55
:entries $ {}
@@ -411,8 +411,9 @@
411411
if (:done? state) nil $ div ({}) (memof1-call-by :abort-streaming comp-abort "\"Streaming...")
412412
if (:done? state)
413413
div
414-
{} $ :class-name (str-spaced css/row-middle)
414+
{} $ :class-name (str-spaced css/row-middle css/gap8)
415415
comp-copy $ :answer state
416+
comp-fill $ either (:answer state) "\""
416417
=< nil 200
417418
comp-message-box (>> states :message-box)
418419
a $ {}
@@ -428,14 +429,22 @@
428429
if dev? $ comp-reel (>> states :reel) reel ({})
429430
if dev? $ comp-inspect "\"Store" store nil
430431
:examples $ []
432+
|comp-fill $ %{} :CodeEntry (:doc |)
433+
:code $ quote
434+
defcomp comp-fill (text)
435+
div $ {} (:class-name style-fill) (:inner-text "|➚")
436+
:on-click $ fn (e d!)
437+
when chrome-extension? $ js/chrome.runtime.sendMessage
438+
js-object (:action |fill-text) (:text text)
439+
:examples $ []
431440
|comp-message-box $ %{} :CodeEntry (:doc |)
432441
:code $ quote
433442
defcomp comp-message-box (states picker-el on-submit)
434443
let
435444
cursor $ :cursor states
436445
state $ either (:data states)
437446
{} (:content "\"") (:search? false) (:think? false)
438-
[] (effect-focus)
447+
[] (effect-focus) (on-fill cursor state on-submit)
439448
div
440449
{} $ :class-name (str-spaced css/center style-message-box-panel)
441450
div
@@ -593,6 +602,22 @@
593602
:code $ quote
594603
def models-menu $ [] (:: :item :gemini-flash "|Gemini Flash 3") (:: :item :gemini-pro "|Gemini Pro 3") (:: :item :gemini-flash-lite "|Gemini Flash Lite 2.5") (:: :item :flash-imagen "\"Flash Imagen") (:: :item :imagen-4 "\"Imagen 4") (:: :item :gemma "|Gemma 3 27b") (:: :item :openrouter/anthropic/claude-sonnet-4.5 "\"Openrouter Claude Sonnet 4.5") (:: :item :openrouter/anthropic/claude-opus-4 "\"Openrouter Claude Opus 4") (:: :item :openrouter/google/gemini-2.5-pro-preview "\"Openrouter Google Gemini 2.5 pro preview") (:: :item :openrouter/google/gemini-2.5-flash-preview-05-20 "\"Openrouter Google Gemini 2.5 flash preview") (:: :item :openrouter/openai/gpt-5 "\"Openrouter GPT 5") (:: :item :openrouter/deepseek/deepseek-chat-v3.1 "\"Openrouter deepseek-chat-v3.1") (; :: :item :claude-4.5 "\"Claude 4.5")
595604
:examples $ []
605+
|on-fill $ %{} :CodeEntry (:doc |)
606+
:code $ quote
607+
defn on-fill (cursor state on-submit)
608+
%{} respo.schema/RespoListener (:name :on-fill)
609+
:handler $ fn (event dispatch!)
610+
tag-match event $
611+
:fill-text info
612+
let
613+
submit? $ either (:submit? info) true
614+
do
615+
dispatch! $ :: :states cursor
616+
assoc state :content $ :text info
617+
if submit?
618+
on-submit (:text info) (:search? state) (:think? state) dispatch!
619+
, nil
620+
:examples $ []
596621
|pattern-spaced-code $ %{} :CodeEntry (:doc |)
597622
:code $ quote
598623
def pattern-spaced-code $ noted "\"temp fix of nested code block" (&raw-code "\"/\\n\\s+```/g")
@@ -639,6 +664,20 @@
639664
defstyle style-code-content $ {}
640665
"\"&" $ {} (:line-height "\"1.5") (:font-size 13)
641666
:examples $ []
667+
|style-fill $ %{} :CodeEntry (:doc |)
668+
:code $ quote
669+
defstyle style-fill $ {}
670+
"\"&" $ {} (:position :relative) (:width 12) (:height 12) (:border-radius "\"2px")
671+
:border $ str "\"1.5px solid " (hsl 200 30 80)
672+
:cursor :pointer
673+
:user-select :none
674+
:display :inline-flex
675+
:align-items :center
676+
:justify-content :center
677+
:font-size 9
678+
:line-height "\"12px"
679+
:color $ hsl 200 70 40
680+
:examples $ []
642681
|style-gap12 $ %{} :CodeEntry (:doc |)
643682
:code $ quote
644683
defstyle style-gap12 $ {}
@@ -805,15 +844,35 @@
805844
:code $ quote
806845
defn listen-extension! ()
807846
js/chrome.runtime.onMessage.addListener $ fn (message sender respond!)
808-
if
809-
= "\"menu-trigger" $ .-action message
847+
when
848+
= "\"menu-summary" $ .-action message
810849
let
811850
content $ str "\"你扮演一个专业的工程师, 对以下内容做一下讲解, 用中文, 注意要简略, 内容注意分块.\n\n" &newline &newline (.-content message)
812-
store $ :store @*reel
813-
cursor $ []
814-
state0 $ get-in store ([] :states :data)
815-
model $ either (:model store) :gemini
816-
submit-message! cursor state0 content false false model dispatch!
851+
event-tuple $ :: :fill-text
852+
{} (:text content) (:submit? true)
853+
(send-to-component! event-tuple)
854+
when
855+
= "\"fill-text" $ .-action message
856+
let
857+
content $ .-text message
858+
submit? $ either (.-submit? message) true
859+
event-tuple $ :: :fill-text
860+
{} (:text content) (:submit? submit?)
861+
(send-to-component! event-tuple)
862+
when
863+
= "\"menu-translate" $ .-action message
864+
let
865+
content $ str "\"请将以下内容翻译成中文, 保持简洁分段:\n\n" &newline &newline (.-content message)
866+
event-tuple $ :: :fill-text
867+
{} (:text content) (:submit? true)
868+
(send-to-component! event-tuple)
869+
when
870+
= "\"menu-custom" $ .-action message
871+
let
872+
content $ .-content message
873+
event-tuple $ :: :fill-text
874+
{} (:text content) (:submit? false)
875+
(send-to-component! event-tuple)
817876
js/chrome.runtime.connect $ js-object (:name |mySidepanel)
818877
:examples $ []
819878
|main! $ %{} :CodeEntry (:doc |)
@@ -877,6 +936,7 @@
877936
app.config :as config
878937
"\"./calcit.build-errors" :default build-errors
879938
"\"bottom-tip" :default hud!
939+
respo.controller.client :refer $ send-to-component!
880940
:examples $ []
881941
|app.schema $ %{} :FileEntry
882942
:defs $ {}

deps.cirru

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
{} (:calcit-version |0.10.7)
2+
{} (:calcit-version |0.10.8)
33
:dependencies $ {} (|Respo/alerts.calcit |0.10.4)
44
|Respo/reel.calcit |main
55
|Respo/respo-markdown.calcit |0.4.11

extension/content.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
66
// Call the specified callback, passing
77
// the web-page's DOM content as argument
88
sendResponse(getSelectedText());
9+
return;
10+
}
11+
if (msg.action === "fill-text") {
12+
insertTextAtCursor(String(msg.text || ""));
913
}
1014
});
1115

@@ -22,3 +26,36 @@ let getSelectedText = () => {
2226
};
2327

2428
console.log("[Side Message] prepared content script");
29+
30+
function insertTextAtCursor(text) {
31+
try {
32+
const active = document.activeElement;
33+
if (!active) return;
34+
35+
if (active.tagName === "INPUT" || active.tagName === "TEXTAREA") {
36+
const input = active;
37+
if (input.readOnly || input.disabled) return;
38+
const start = input.selectionStart ?? input.value.length;
39+
const end = input.selectionEnd ?? input.value.length;
40+
input.setRangeText(text, start, end, "end");
41+
input.dispatchEvent(new Event("input", { bubbles: true }));
42+
return;
43+
}
44+
45+
if (active.isContentEditable) {
46+
const sel = window.getSelection();
47+
if (!sel || sel.rangeCount === 0) return;
48+
const range = sel.getRangeAt(0);
49+
if (!active.contains(range.commonAncestorContainer)) return;
50+
range.deleteContents();
51+
const textNode = document.createTextNode(text);
52+
range.insertNode(textNode);
53+
range.setStartAfter(textNode);
54+
range.setEndAfter(textNode);
55+
sel.removeAllRanges();
56+
sel.addRange(range);
57+
}
58+
} catch (err) {
59+
console.error("[Side Message] failed to insert text", err);
60+
}
61+
}

extension/service-worker.js

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,73 @@ chrome.runtime.onInstalled.addListener(() => {
66

77
chrome.runtime.onInstalled.addListener(async () => {
88
chrome.contextMenus.create({
9-
id: "msg-gemeni-selection",
9+
id: "msg-gemini-root",
1010
title: "Msg Gemini",
1111
type: "normal",
1212
contexts: ["selection"],
1313
});
14+
chrome.contextMenus.create({
15+
id: "msg-gemini-summary",
16+
title: "Summary",
17+
type: "normal",
18+
contexts: ["selection"],
19+
parentId: "msg-gemini-root",
20+
});
21+
chrome.contextMenus.create({
22+
id: "msg-gemini-translate",
23+
title: "Translate",
24+
type: "normal",
25+
contexts: ["selection"],
26+
parentId: "msg-gemini-root",
27+
});
28+
chrome.contextMenus.create({
29+
id: "msg-gemini-custom",
30+
title: "Custom...",
31+
type: "normal",
32+
contexts: ["selection"],
33+
parentId: "msg-gemini-root",
34+
});
1435
});
1536

1637
chrome.contextMenus.onClicked.addListener((item, tab) => {
1738
let content = item.selectionText;
18-
chrome.runtime.sendMessage({ action: "menu-trigger", content });
39+
if (item.menuItemId === "msg-gemini-translate") {
40+
chrome.runtime.sendMessage({ action: "menu-translate", content });
41+
} else if (item.menuItemId === "msg-gemini-custom") {
42+
chrome.runtime.sendMessage({ action: "menu-custom", content });
43+
} else {
44+
chrome.runtime.sendMessage({ action: "menu-summary", content });
45+
}
1946
chrome.sidePanel.open({ tabId: tab.id }, () => {
2047
// also try to open
2148
if (!sidepanelOpen) {
2249
setTimeout(() => {
23-
chrome.runtime.sendMessage({ action: "menu-trigger", content });
50+
if (item.menuItemId === "msg-gemini-translate") {
51+
chrome.runtime.sendMessage({ action: "menu-translate", content });
52+
} else if (item.menuItemId === "msg-gemini-custom") {
53+
chrome.runtime.sendMessage({ action: "menu-custom", content });
54+
} else {
55+
chrome.runtime.sendMessage({ action: "menu-summary", content });
56+
}
2457
}, 1000);
2558
}
2659
});
2760
});
2861

62+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
63+
if (message && message.action === "fill-text") {
64+
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
65+
const tab = tabs && tabs[0];
66+
if (tab && tab.id != null) {
67+
chrome.tabs.sendMessage(tab.id, {
68+
action: "fill-text",
69+
text: message.text || "",
70+
});
71+
}
72+
});
73+
}
74+
});
75+
2976
// https://stackoverflow.com/a/77106777/883571
3077
chrome.runtime.onConnect.addListener(function (port) {
3178
if (port.name === "mySidepanel") {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"dependencies": {
3-
"@calcit/procs": "^0.10.7",
3+
"@calcit/procs": "^0.10.8",
44
"@google/genai": "^1.36.0",
55
"axios": "^1.12.2",
66
"cirru-color": "^0.2.4",

yarn.lock

Lines changed: 6 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)