Skip to content

Commit 867d369

Browse files
est7dehesa
authored andcommitted
feat: add script to open gemini tab and submit prompt
Implement a Node.js script that enables seamless interaction with Google Gemini from Raycast. The script allows users to: - Open Gemini in Chrome browser - Submit prompts with optional selected text as context - Work with existing Gemini tabs without opening duplicates Dependencies: Node.js and chrome-cli feat: add script to open gemini tab and submit prompt Implement a Node.js script that enables seamless interaction with Google Gemini from Raycast. The script allows users to: - Open Gemini in Chrome browser - Submit prompts with optional selected text as context - Work with existing Gemini tabs without opening duplicates Dependencies: Node.js and chrome-cli Delete gemini.js
1 parent e465aa6 commit 867d369

File tree

2 files changed

+215
-0
lines changed

2 files changed

+215
-0
lines changed

commands/ai/gemini/gemini.js

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#!/usr/bin/env node
2+
3+
// Dependencies:
4+
// This script requires the following software to be installed:
5+
// - `node` https://nodejs.org
6+
// - `chrome-cli` https://github.com/prasmussen/chrome-cli
7+
// Install via homebrew: `brew install node chrome-cli`
8+
9+
// This script needs to run JavaScript in your browser, which requires your permission.
10+
// To do so, open Chrome and find the menu bar item:
11+
// View > Developer > Allow JavaScript from Apple Events
12+
13+
// Required parameters:
14+
// @raycast.schemaVersion 1
15+
// @raycast.title Ask Gemini
16+
// @raycast.mode silent
17+
// @raycast.packageName Gemini
18+
19+
// Optional parameters:
20+
// @raycast.icon ../images/icon-gemini.svg
21+
// @raycast.argument1 { "type": "text", "placeholder": "Selected Text", "optional": true }
22+
// @raycast.argument2 { "type": "text", "placeholder": "Prompt"}
23+
24+
// Documentation:
25+
// @raycast.description Open Gemini in Chrome browser and submit a prompt with optional selected text as context
26+
// @raycast.author Modified from Nimo Beeren's Claude script
27+
28+
const { execSync } = require("child_process");
29+
30+
const selectedText = process.argv[2] || ""; // Get the selected text, or an empty string if none is selected.
31+
const prompt = process.argv[3];
32+
33+
process.env.OUTPUT_FORMAT = "json";
34+
35+
/** Escape a string so that it can be used in JavaScript code when wrapped in double quotes. */
36+
function escapeJsString(str) {
37+
return str.replaceAll(`\\`, `\\\\`).replaceAll(`"`, `\\"`);
38+
}
39+
40+
/** Escape a string so that it can be used in a shell command when wrapped in single quotes. */
41+
function escapeShellString(str) {
42+
return str.replaceAll(`'`, `'"'"'`);
43+
}
44+
45+
// used to wait for Chrome to activate.
46+
function sleep(ms) {
47+
const start = Date.now();
48+
while (Date.now() - start < ms) {}
49+
}
50+
51+
try {
52+
execSync("which chrome-cli");
53+
} catch {
54+
console.error(
55+
"chrome-cli is required to run this script (https://github.com/prasmussen/chrome-cli)",
56+
);
57+
process.exit(1);
58+
}
59+
60+
// Bring Chrome to the foreground first.
61+
try {
62+
// Try to activate Chrome through AppleScript, supporting different possible application names.
63+
execSync("osascript -e 'tell application \"Google Chrome\" to activate'", {
64+
stdio: "ignore",
65+
});
66+
} catch (e) {
67+
try {
68+
// If the first naming method fails, try possible alternatives.
69+
execSync("osascript -e 'tell application \"Chrome\" to activate'", {
70+
stdio: "ignore",
71+
});
72+
} catch (err) {
73+
console.error(
74+
"Unable to activate Chrome browser, continue with other operations",
75+
);
76+
}
77+
}
78+
79+
// Give Chrome a little time to make sure it is activated
80+
sleep(300);
81+
82+
// Find the Gemini tab if one is already open
83+
let tabs = JSON.parse(execSync("chrome-cli list tabs")).tabs;
84+
let geminiTab = tabs.find((tab) =>
85+
tab.url.startsWith("https://gemini.google.com/"),
86+
);
87+
88+
// If there is a Gemini tab open, get its info. Otherwise, open Gemini in a new window.
89+
let geminiTabInfo;
90+
if (geminiTab) {
91+
// Focus on existing tags, do not refresh the page
92+
execSync(`chrome-cli activate -t ${geminiTab.id}`);
93+
// Get tab info
94+
geminiTabInfo = JSON.parse(execSync(`chrome-cli info -t ${geminiTab.id}`));
95+
} else {
96+
// Open a Gemini session in a new tab, focus it and return the tab info
97+
geminiTabInfo = JSON.parse(
98+
execSync("chrome-cli open 'https://gemini.google.com/app'"),
99+
);
100+
}
101+
102+
// Wait for the tab to be loaded, then execute the script
103+
let interval = setInterval(() => {
104+
if (geminiTabInfo.loading) {
105+
geminiTabInfo = JSON.parse(
106+
execSync(`chrome-cli info -t ${geminiTabInfo.id}`),
107+
);
108+
} else {
109+
clearInterval(interval);
110+
executeScript();
111+
}
112+
}, 100);
113+
114+
function executeScript() {
115+
const script = async function (selectedText, prompt) {
116+
// Wait for prompt element to be on the page
117+
let promptElement;
118+
await new Promise((resolve) => {
119+
let interval = setInterval(() => {
120+
promptElement = document.querySelector(
121+
'div[aria-label="Enter a prompt here"]',
122+
);
123+
if (promptElement) {
124+
clearInterval(interval);
125+
resolve();
126+
}
127+
}, 100);
128+
});
129+
130+
// Prepare the final text
131+
let finalText = "";
132+
if (selectedText && selectedText.trim() !== "") {
133+
finalText += `<file_content>${selectedText}</file_contents>\n\n${prompt}`;
134+
} else {
135+
finalText = prompt;
136+
}
137+
138+
// Focus the input element first
139+
promptElement.focus();
140+
141+
// Check if there's existing content
142+
const hasExistingContent = promptElement.textContent.trim() !== "";
143+
144+
// Clear existing content if needed - safely without innerHTML
145+
if (!hasExistingContent) {
146+
// If empty, we'll just add our content
147+
// No need to clear anything
148+
} else {
149+
// If we want to append to existing content, add a newline
150+
// Create a new paragraph for separation
151+
const selection = window.getSelection();
152+
const range = document.createRange();
153+
154+
// Move cursor to the end of existing content
155+
range.selectNodeContents(promptElement);
156+
range.collapse(false); // false means collapse to end
157+
selection.removeAllRanges();
158+
selection.addRange(range);
159+
160+
// Insert two newlines to separate content
161+
document.execCommand("insertText", false, "\n\n");
162+
}
163+
164+
// Insert the content using execCommand which is safer than innerHTML
165+
// Split by newlines and insert with proper paragraph formatting
166+
const paragraphs = finalText.split("\n");
167+
paragraphs.forEach((paragraph, index) => {
168+
if (index > 0) {
169+
// Insert newline between paragraphs (not before the first one)
170+
document.execCommand("insertText", false, "\n");
171+
}
172+
173+
// Insert the paragraph text
174+
document.execCommand("insertText", false, paragraph || "\u200B");
175+
});
176+
177+
// Trigger input event to notify Gemini of changes
178+
const inputEvent = new Event("input", { bubbles: true });
179+
promptElement.dispatchEvent(inputEvent);
180+
181+
// Ensure cursor is at the end and visible
182+
const selection = window.getSelection();
183+
const range = document.createRange();
184+
range.selectNodeContents(promptElement);
185+
range.collapse(false); // false means collapse to end
186+
selection.removeAllRanges();
187+
selection.addRange(range);
188+
189+
// Scroll to make cursor visible
190+
promptElement.scrollTop = promptElement.scrollHeight;
191+
192+
// Additional scroll after a short delay to ensure visibility
193+
setTimeout(() => {
194+
promptElement.scrollTop = promptElement.scrollHeight;
195+
}, 100);
196+
};
197+
198+
const functionString = escapeShellString(script.toString());
199+
const selectedTextString = escapeShellString(escapeJsString(selectedText));
200+
const promptString = escapeShellString(escapeJsString(prompt));
201+
202+
execSync(
203+
`chrome-cli execute '(${functionString})(\"${selectedTextString}\", \"${promptString}\")' -t ${geminiTabInfo.id}`,
204+
);
205+
}

commands/ai/images/icon-gemini.svg

Lines changed: 10 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)