Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
570 changes: 456 additions & 114 deletions Agents.md

Large diffs are not rendered by default.

80 changes: 70 additions & 10 deletions compact.cirru
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

{} (:package |app)
{} (:about "|file is generated - never edit directly; learn cr edit/tree workflows before changing") (:package |app)
:configs $ {} (:init-fn |app.main/main!) (:reload-fn |app.main/reload!) (:version |0.0.1)
:modules $ [] |respo.calcit/ |lilac/ |memof/ |respo-ui.calcit/ |reel.calcit/ |respo-markdown.calcit/ |alerts.calcit/
:entries $ {}
Expand Down Expand Up @@ -411,8 +411,9 @@
if (:done? state) nil $ div ({}) (memof1-call-by :abort-streaming comp-abort "\"Streaming...")
if (:done? state)
div
{} $ :class-name (str-spaced css/row-middle)
{} $ :class-name (str-spaced css/row-middle css/gap8)
comp-copy $ :answer state
comp-fill $ either (:answer state) "\""
=< nil 200
comp-message-box (>> states :message-box)
a $ {}
Expand All @@ -428,14 +429,22 @@
if dev? $ comp-reel (>> states :reel) reel ({})
if dev? $ comp-inspect "\"Store" store nil
:examples $ []
|comp-fill $ %{} :CodeEntry (:doc |)
:code $ quote
defcomp comp-fill (text)
div $ {} (:class-name style-fill) (:inner-text "|➚")
:on-click $ fn (e d!)
when chrome-extension? $ js/chrome.runtime.sendMessage
js-object (:action |fill-text) (:text text)
:examples $ []
|comp-message-box $ %{} :CodeEntry (:doc |)
:code $ quote
defcomp comp-message-box (states picker-el on-submit)
let
cursor $ :cursor states
state $ either (:data states)
{} (:content "\"") (:search? false) (:think? false)
[] (effect-focus)
[] (effect-focus) (on-fill cursor state on-submit)
div
{} $ :class-name (str-spaced css/center style-message-box-panel)
div
Expand Down Expand Up @@ -593,6 +602,22 @@
:code $ quote
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")
:examples $ []
|on-fill $ %{} :CodeEntry (:doc |)
:code $ quote
defn on-fill (cursor state on-submit)
%{} respo.schema/RespoListener (:name :on-fill)
:handler $ fn (event dispatch!)
tag-match event $
:fill-text info
let
submit? $ either (:submit? info) true
do
dispatch! $ :: :states cursor
assoc state :content $ :text info
if submit?
on-submit (:text info) (:search? state) (:think? state) dispatch!
, nil
:examples $ []
|pattern-spaced-code $ %{} :CodeEntry (:doc |)
:code $ quote
def pattern-spaced-code $ noted "\"temp fix of nested code block" (&raw-code "\"/\\n\\s+```/g")
Expand Down Expand Up @@ -639,6 +664,20 @@
defstyle style-code-content $ {}
"\"&" $ {} (:line-height "\"1.5") (:font-size 13)
:examples $ []
|style-fill $ %{} :CodeEntry (:doc |)
:code $ quote
defstyle style-fill $ {}
"\"&" $ {} (:position :relative) (:width 12) (:height 12) (:border-radius "\"2px")
:border $ str "\"1.5px solid " (hsl 200 30 80)
:cursor :pointer
:user-select :none
:display :inline-flex
:align-items :center
:justify-content :center
:font-size 9
:line-height "\"12px"
:color $ hsl 200 70 40
:examples $ []
|style-gap12 $ %{} :CodeEntry (:doc |)
:code $ quote
defstyle style-gap12 $ {}
Expand Down Expand Up @@ -805,15 +844,35 @@
:code $ quote
defn listen-extension! ()
js/chrome.runtime.onMessage.addListener $ fn (message sender respond!)
if
= "\"menu-trigger" $ .-action message
when
= "\"menu-summary" $ .-action message
let
content $ str "\"你扮演一个专业的工程师, 对以下内容做一下讲解, 用中文, 注意要简略, 内容注意分块.\n\n" &newline &newline (.-content message)
store $ :store @*reel
cursor $ []
state0 $ get-in store ([] :states :data)
model $ either (:model store) :gemini
submit-message! cursor state0 content false false model dispatch!
event-tuple $ :: :fill-text
{} (:text content) (:submit? true)
(send-to-component! event-tuple)
when
= "\"fill-text" $ .-action message
let
content $ .-text message
submit? $ either (.-submit? message) true
event-tuple $ :: :fill-text
{} (:text content) (:submit? submit?)
(send-to-component! event-tuple)
when
= "\"menu-translate" $ .-action message
let
content $ str "\"请将以下内容翻译成中文, 保持简洁分段:\n\n" &newline &newline (.-content message)
event-tuple $ :: :fill-text
{} (:text content) (:submit? true)
(send-to-component! event-tuple)
when
= "\"menu-custom" $ .-action message
let
content $ .-content message
event-tuple $ :: :fill-text
{} (:text content) (:submit? false)
(send-to-component! event-tuple)
js/chrome.runtime.connect $ js-object (:name |mySidepanel)
:examples $ []
|main! $ %{} :CodeEntry (:doc |)
Expand Down Expand Up @@ -877,6 +936,7 @@
app.config :as config
"\"./calcit.build-errors" :default build-errors
"\"bottom-tip" :default hud!
respo.controller.client :refer $ send-to-component!
:examples $ []
|app.schema $ %{} :FileEntry
:defs $ {}
Expand Down
8 changes: 4 additions & 4 deletions deps.cirru
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@

{} (:calcit-version |0.10.4)
:dependencies $ {} (|Respo/alerts.calcit |0.10.2)
{} (:calcit-version |0.10.8)
:dependencies $ {} (|Respo/alerts.calcit |0.10.4)
|Respo/reel.calcit |main
|Respo/respo-markdown.calcit |0.4.11
|Respo/respo-ui.calcit |0.6.3
|Respo/respo.calcit |0.16.22
|Respo/respo.calcit |0.16.24
|calcit-lang/lilac |main
|calcit-lang/memof |main
|calcit-lang/memof |0.0.17
37 changes: 37 additions & 0 deletions extension/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
// 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 || ""));
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing return statement after handling the 'fill-text' action. While JavaScript will implicitly return undefined, this is inconsistent with the explicit return statement at line 9 for the 'selected' action. For consistency and clarity, add an explicit return statement after calling insertTextAtCursor.

Suggested change
insertTextAtCursor(String(msg.text || ""));
insertTextAtCursor(String(msg.text || ""));
return;

Copilot uses AI. Check for mistakes.
}
});

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

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);
}
}
53 changes: 50 additions & 3 deletions extension/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,73 @@ chrome.runtime.onInstalled.addListener(() => {

chrome.runtime.onInstalled.addListener(async () => {
chrome.contextMenus.create({
id: "msg-gemeni-selection",
id: "msg-gemini-root",
title: "Msg Gemini",
type: "normal",
contexts: ["selection"],
});
chrome.contextMenus.create({
id: "msg-gemini-summary",
title: "Summary",
type: "normal",
contexts: ["selection"],
parentId: "msg-gemini-root",
});
chrome.contextMenus.create({
id: "msg-gemini-translate",
title: "Translate",
type: "normal",
contexts: ["selection"],
parentId: "msg-gemini-root",
});
chrome.contextMenus.create({
id: "msg-gemini-custom",
title: "Custom...",
type: "normal",
contexts: ["selection"],
parentId: "msg-gemini-root",
});
});

chrome.contextMenus.onClicked.addListener((item, tab) => {
let content = item.selectionText;
chrome.runtime.sendMessage({ action: "menu-trigger", content });
if (item.menuItemId === "msg-gemini-translate") {
chrome.runtime.sendMessage({ action: "menu-translate", content });
} else if (item.menuItemId === "msg-gemini-custom") {
chrome.runtime.sendMessage({ action: "menu-custom", content });
} else {
chrome.runtime.sendMessage({ action: "menu-summary", content });
}
chrome.sidePanel.open({ tabId: tab.id }, () => {
// also try to open
if (!sidepanelOpen) {
setTimeout(() => {
chrome.runtime.sendMessage({ action: "menu-trigger", content });
if (item.menuItemId === "msg-gemini-translate") {
chrome.runtime.sendMessage({ action: "menu-translate", content });
} else if (item.menuItemId === "msg-gemini-custom") {
chrome.runtime.sendMessage({ action: "menu-custom", content });
} else {
chrome.runtime.sendMessage({ action: "menu-summary", content });
}
}, 1000);
}
});
});
Comment thread
tiye marked this conversation as resolved.

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 || "",
});
Comment on lines +67 to +70
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for chrome.tabs.sendMessage. If the message fails to send (e.g., content script not loaded on the page), this will fail silently. Consider adding error handling or checking chrome.runtime.lastError in a callback to provide feedback when text filling fails.

Suggested change
chrome.tabs.sendMessage(tab.id, {
action: "fill-text",
text: message.text || "",
});
chrome.tabs.sendMessage(
tab.id,
{
action: "fill-text",
text: message.text || "",
},
(response) => {
if (chrome.runtime.lastError) {
console.error(
"Failed to send 'fill-text' message to tab",
tab.id,
chrome.runtime.lastError.message
);
}
}
);

Copilot uses AI. Check for mistakes.
}
});
}
});

// https://stackoverflow.com/a/77106777/883571
chrome.runtime.onConnect.addListener(function (port) {
if (port.name === "mySidepanel") {
Expand Down
Loading
Loading