// ==UserScript==
// @name Gemini Prompt Queue
// @description Queue multiple prompts for Gemini
// @author nihaltp
// @namespace https://github.com/nihaltp/uscripts
// @supportURL https://github.com/nihaltp/uscripts/issues
// @homepageURL https://github.com/nihaltp/uscripts
// @homepage https://github.com/nihaltp/uscripts
// @license MIT
// @match https://gemini.google.com/*
// @icon https://gemini.google.com/favicon.ico
// @version 1.0.0
// @grant none
// @downloadURL https://raw.githubusercontent.com/nihaltp/uscripts/main/AI Queue/gemini.user.js
// @updateURL https://raw.githubusercontent.com/nihaltp/uscripts/main/AI Queue/gemini.user.js
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const queue = [];
let running = false;
let editingId = null;
let draggedId = null;
window.aiQueueDebug = true; // set to true to enable debug logs
// -----------------------------
// MARK: UI
// -----------------------------
let panel;
let isPanelVisible = false;
// MARK: logging helpers
function log(...args) {
if (!window.aiQueueDebug) return;
console.log("[AI QUEUE]", ...args);
}
function error(...args) {
console.error("[AI QUEUE]", ...args);
}
function throwError(...args) {
error(...args);
throw new Error(args.join(' '));
}
// MARK: create panel
function createPanel() {
panel = document.createElement('div');
panel.id = 'pq-panel';
Object.assign(panel.style, {
position: 'fixed',
top: '100px',
left: '100px',
bottom: 'auto',
right: 'auto',
width: '320px',
minHeight: '200px',
maxHeight: '70vh',
overflowY: 'auto',
background: '#202123',
color: 'white',
border: '1px solid #444',
borderRadius: '16px',
padding: '12px',
zIndex: '2147483647',
boxShadow: '0 10px 40px rgba(0,0,0,0.6)',
display: 'none',
// outline: '2px solid #555',
});
panel.innerHTML = `
Prompt Queue
Idle
`;
// document.documentElement.appendChild(panel);
document.body.appendChild(panel);
setupPanelEvents();
}
// MARK: setupPanelEvents
function setupPanelEvents() {
const input = panel.querySelector('#pq-input');
const addBtn = panel.querySelector('#pq-add');
const startBtn = panel.querySelector('#pq-start');
window.pqInput = input;
window.pqAddBtn = addBtn;
addBtn.addEventListener('click', () => {
const text = input.value.trim();
if (!text) {
error('Empty prompt, not adding to queue');
return;
}
// editing existing item
if (editingId !== null) {
const item = queue.find(item => item.id === editingId);
if (!item) {
error('Editing item not found in queue:', editingId);
return;
}
item.prompt = text;
editingId = null;
addBtn.textContent = 'Add To Queue';
} else {
// add new item
queue.push({
id: crypto.randomUUID(),
prompt: text,
});
}
updateToolbarButton();
input.value = '';
renderQueue();
});
startBtn.addEventListener('click', async () => {
if (running) return;
running = true;
updateToolbarButton();
processQueue();
});
}
// MARK: renderQueue
function renderQueue() {
const list = panel.querySelector('#pq-list');
list.innerHTML = '';
queue.forEach((item, index) => {
const li = document.createElement('li');
li.style.marginBottom = '10px';
li.draggable = true;
const row = document.createElement('div');
row.style.display = 'flex';
row.style.gap = '6px';
row.style.alignItems = 'flex-start';
if (editingId == item.id) {
row.style.background = '#333';
row.style.padding = '6px';
row.style.borderRadius = '6px';
row.style.outline = '1px solid #7dd3fc';
}
const text = document.createElement('div');
text.textContent = item.prompt;
text.style.flex = '1';
text.style.wordBreak = 'break-word';
text.style.fontSize = '14px';
text.addEventListener('dblclick', () => {
editQueueItem(item.id);
});
const editBtn = document.createElement('button');
editBtn.textContent = '🖉';
editBtn.title = 'Edit';
editBtn.style.cursor = 'pointer';
editBtn.style.color = '#7dd3fc';
editBtn.addEventListener('click', () => {
editQueueItem(item.id);
});
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '✕';
deleteBtn.title = 'Delete';
deleteBtn.style.cursor = 'pointer';
deleteBtn.style.color = '#ff6b6b';
deleteBtn.addEventListener('click', () => {
const preview =
item.prompt.length > 80
? item.prompt.slice(0, 80) + '...'
: item.prompt;
const confirmed = confirm(
`Delete this prompt?\n\n${preview}`
);
if (!confirmed) {
error('Deletion cancelled for item:', item.id);
return;
}
deleteQueueItem(item.id);
});
row.appendChild(text);
row.appendChild(editBtn);
row.appendChild(deleteBtn);
li.appendChild(row);
li.addEventListener('dragstart', () => {
draggedId = item.id;
li.style.opacity = '0.5';
});
li.addEventListener('dragend', () => {
draggedId = null;
li.style.opacity = '1';
});
li.addEventListener('dragover', e => {
e.preventDefault();
li.style.borderTop = '2px solid #888';
});
li.addEventListener('dragleave', () => {
li.style.borderTop = '';
});
li.addEventListener('drop', e => {
e.preventDefault();
li.style.borderTop = '';
if (draggedId === item.id) {
error('Dropped on itself, ignoring:', item.id);
return;
}
moveQueueItem(draggedId, item.id);
});
list.appendChild(li);
});
updateToolbarButton();
}
// MARK: deleteQueueItem
function deleteQueueItem(id) {
const index = queue.findIndex(item => item.id === id);
if (index === -1) {
error('Item to delete not found in queue:', id);
return;
}
queue.splice(index, 1);
renderQueue();
}
// MARK: editQueueItem
function editQueueItem(id) {
const item = queue.find(item => item.id === id);
if (!item) {
error('Item to edit not found in queue:', id);
return;
}
editingId = id;
window.pqInput.value = item.prompt;
window.pqAddBtn.textContent = 'Save Changes';
window.pqInput.focus();
// move cursor to end
window.pqInput.selectionStart =
window.pqInput.selectionEnd =
window.pqInput.value.length;
}
// MARK: moveQueueItem
function moveQueueItem(fromId, toId) {
const fromIndex = queue.findIndex(item => item.id === fromId);
const toIndex = queue.findIndex(item => item.id === toId);
if (fromIndex === -1 || toIndex === -1) {
return;
}
const [movedItem] = queue.splice(fromIndex, 1);
queue.splice(toIndex, 0, movedItem);
renderQueue();
}
// MARK: setStatus
function setStatus(text) {
const status = panel.querySelector('#pq-status');
if (status) {
status.textContent = text;
}
}
// MARK: togglePanel
function togglePanel() {
isPanelVisible = !isPanelVisible;
panel.style.display =
isPanelVisible ? 'block' : 'none';
panel.style.pointerEvents =
isPanelVisible ? 'auto' : 'none';
panel.style.visibility = 'visible';
panel.style.opacity = '1';
panel.style.top = '100px';
panel.style.left = '100px';
panel.style.right = 'auto';
panel.style.bottom = 'auto';
panel.style.inset = 'unset';
log('Panel element:', panel);
log('Panel visible:', isPanelVisible);
}
// MARK: createToolbarButton
function createToolbarButton() {
const host = getComposerHost();
let button = document.querySelector('#pq-toolbar-button');
if (!button) {
button = document.createElement('button');
button.id = 'pq-toolbar-button';
button.type = 'button';
button.textContent = 'Queue';
button.addEventListener('click', togglePanel);
// add animation styles once
if (!document.querySelector('#pq-styles')) {
const style = document.createElement('style');
style.id = 'pq-styles';
style.textContent = `
@keyframes pq-pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.06);
opacity: 0.75;
}
100% {
transform: scale(1);
opacity: 1;
}
}
`;
document.head.appendChild(style);
}
}
if (host) {
button.className = 'composer-btn h-9 min-h-9';
button.style.position = '';
button.style.bottom = '';
button.style.right = '';
button.style.left = '';
button.style.zIndex = '';
button.style.padding = '0 12px';
button.style.borderRadius = '9999px';
button.style.marginInlineStart = '8px';
if (button.parentElement !== host) {
host.prepend(button);
}
} else {
button.className = '';
button.style.position = 'fixed';
button.style.bottom = '24px';
button.style.right = '24px';
button.style.padding = '10px 14px';
button.style.borderRadius = '9999px';
button.style.background = '#1f1f1f';
button.style.color = '#fff';
button.style.border = '1px solid #555';
button.style.boxShadow = '0 10px 30px rgba(0,0,0,0.35)';
button.style.zIndex = '2147483647';
if (button.parentElement !== document.body) {
document.body.appendChild(button);
}
}
}
// MARK: updateToolbarButton
function updateToolbarButton() {
const button = document.querySelector('#pq-toolbar-button');
if (!button) {
error('Toolbar button not found, cannot update');
return;
}
const count = queue.length;
button.textContent =
count > 0
? `Queue (${count})`
: 'Queue';
// running animation
if (running) {
button.style.animation = 'pq-pulse 1.2s infinite';
button.style.opacity = '1';
} else {
button.style.animation = '';
button.style.opacity = count > 0 ? '1' : '0.8';
}
}
createPanel();
// continuously reattach button because Gemini rerenders UI
const observer = new MutationObserver(() => {
createToolbarButton();
log('DOM mutated, ensured toolbar button and panel exist');
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
// -----------------------------
// MARK: Gemini Helpers
// -----------------------------
function getComposerEditor() {
return document.querySelector(
'#prompt-textarea, textarea:not(#pq-input), [contenteditable="true"][role="textbox"], [contenteditable="true"]'
);
}
function getComposerHost() {
const editor = getComposerEditor();
if (!editor) {
return null;
}
return (
editor.closest('form') ||
editor.closest('[role="toolbar"]') ||
editor.closest('div') ||
editor.parentElement
);
}
// MARK: getSendButton
function getSendButton() {
const selectors = [
'button[data-testid="send-button"]',
'button[aria-label*="Send"]',
'button[title*="Send"]',
'button[aria-label*="Submit"]',
'[role="button"][aria-label*="Send"]',
];
for (const selector of selectors) {
const button = document.querySelector(selector);
if (button && !button.disabled && button.offsetParent !== null) {
return button;
}
}
const host = getComposerHost();
if (!host) {
return null;
}
const buttons = [...host.querySelectorAll('button,[role="button"]')];
return (
buttons.find(btn =>
!btn.disabled &&
btn.offsetParent !== null &&
/send|submit/i.test((btn.getAttribute('aria-label') || '') + ' ' + (btn.textContent || '') + ' ' + (btn.getAttribute('title') || ''))
) || null
);
}
function isGenerating() {
return !!document.querySelector(
'button[data-testid="stop-button"], button[aria-label*="Stop"], [role="button"][aria-label*="Stop"]'
);
}
async function waitForIdle() {
while (isGenerating()) {
await sleep(1000);
}
// extra delay for stability
await sleep(1500);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function setEditorValue(editor, prompt) {
if (!editor) {
throwError('Editor not found');
}
editor.focus();
if ('value' in editor) {
editor.value = prompt;
editor.dispatchEvent(
new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: prompt,
})
);
return;
}
if (editor.isContentEditable) {
editor.textContent = prompt;
editor.dispatchEvent(
new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: prompt,
})
);
return;
}
throwError('Unsupported editor type');
}
// MARK: sendPrompt
async function sendPrompt(prompt) {
const editor = getComposerEditor();
if (!editor) {
throwError('Editor not found');
}
setEditorValue(editor, prompt);
await sleep(800);
const sendButton = getSendButton();
if (sendButton) {
sendButton.click();
return;
}
editor.dispatchEvent(
new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
key: 'Enter',
code: 'Enter',
})
);
}
// MARK: processQueue
async function processQueue() {
setStatus('Running');
while (queue.length > 0) {
await waitForIdle();
const item = queue.shift();
const prompt = item.prompt;
updateToolbarButton();
renderQueue();
setStatus(`Sending: ${prompt.slice(0, 40)}...`);
try {
await sendPrompt(prompt);
await sleep(1000);
await waitForIdle();
} catch (err) {
error('Error processing prompt:', err);
setStatus('Error: ' + err.message);
running = false;
updateToolbarButton();
return;
}
}
setStatus('Finished');
running = false;
updateToolbarButton();
}
})();