Skip to content

Instantly share code, notes, and snippets.

@happyjake
Created February 16, 2026 04:52
Show Gist options
  • Select an option

  • Save happyjake/f5302f4d583ace3da293dc12434fbf21 to your computer and use it in GitHub Desktop.

Select an option

Save happyjake/f5302f4d583ace3da293dc12434fbf21 to your computer and use it in GitHub Desktop.
Gemini Paste — Tampermonkey userscript that adds a Paste button to Gemini code blocks (uploads to paste.dzzu.net via MCP)
// ==UserScript==
// @name Gemini Paste
// @namespace https://github.com/happyjake
// @version 1.0.0
// @description Adds a "Paste" button to Gemini code blocks that uploads to paste.dzzu.net
// @match https://gemini.google.com/*
// @grant GM_xmlhttpRequest
// @connect paste.dzzu.net
// ==/UserScript==
(function () {
"use strict";
const style = document.createElement("style");
style.textContent = `@keyframes gp-spin{to{transform:rotate(360deg)}}
.paste-button{position:relative}
.paste-button::after{content:attr(data-tooltip);position:absolute;top:100%;left:50%;transform:translateX(-50%);margin-top:8px;padding:4px 8px;background:#e8eaed;color:#1f1f1f;font-size:12px;font-family:Google Sans,Roboto,sans-serif;border-radius:4px;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .15s}
.paste-button:hover::after{opacity:1}`;
document.head.appendChild(style);
const PASTE_URL = "https://paste.dzzu.net/mcp";
const PROCESSED = "data-paste-btn";
// --- MCP Client (from text-explainer.user.js) ---
const mcpSessions = new Map();
let mcpReqId = 0;
function parseMcpSseResponse(text) {
if (!text) return null;
const dataLines = [];
for (const line of text.split("\n")) {
if (line.startsWith("data: ")) dataLines.push(line.slice(6));
else if (line.startsWith("data:")) dataLines.push(line.slice(5));
}
if (dataLines.length) {
try { return JSON.parse(dataLines.join("\n")); } catch {}
}
try { return JSON.parse(text); } catch {}
return null;
}
function extractHeader(headers, name) {
const lower = name.toLowerCase();
for (const line of headers.split("\n")) {
const idx = line.indexOf(":");
if (idx > 0 && line.slice(0, idx).trim().toLowerCase() === lower)
return line.slice(idx + 1).trim();
}
return null;
}
function mcpRequest(url, body, sessionId) {
return new Promise((resolve, reject) => {
const headers = { "Content-Type": "application/json", Accept: "application/json, text/event-stream" };
if (sessionId) headers["mcp-session-id"] = sessionId;
GM_xmlhttpRequest({
method: "POST", url, headers, data: JSON.stringify(body),
onload: (res) => {
if (res.status < 200 || res.status >= 300) return reject(new Error(`MCP HTTP ${res.status}`));
resolve({
data: parseMcpSseResponse(res.responseText),
sessionId: extractHeader(res.responseHeaders || "", "mcp-session-id"),
});
},
onerror: () => reject(new Error("MCP network error")),
ontimeout: () => reject(new Error("MCP timeout")),
timeout: 15000,
});
});
}
async function mcpInitialize(url) {
const cached = mcpSessions.get(url);
if (cached) return cached;
const { data, sessionId } = await mcpRequest(url, {
jsonrpc: "2.0", id: ++mcpReqId, method: "initialize",
params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "gemini-paste", version: "1.0.0" } },
});
const sid = sessionId || data?.result?.sessionId || null;
mcpSessions.set(url, sid);
mcpRequest(url, { jsonrpc: "2.0", method: "notifications/initialized" }, sid).catch(() => {});
return sid;
}
async function mcpCallTool(url, sid, tool, args) {
const call = async (s) => {
const { data } = await mcpRequest(url, {
jsonrpc: "2.0", id: ++mcpReqId, method: "tools/call",
params: { name: tool, arguments: args },
}, s);
return data?.result?.content?.filter(c => c.type === "text").map(c => c.text).join("\n")
|| JSON.stringify(data?.result || data);
};
try { return await call(sid); } catch (e) {
console.warn("[gemini-paste] Retrying with fresh session:", e.message);
mcpSessions.delete(url);
return call(await mcpInitialize(url));
}
}
// --- Language → extension map ---
const EXT_MAP = {
javascript: "js", typescript: "ts", python: "py", java: "java", kotlin: "kt",
rust: "rs", go: "go", ruby: "rb", php: "php", swift: "swift", csharp: "cs",
"c++": "cpp", c: "c", html: "html", css: "css", json: "json", yaml: "yaml",
xml: "xml", sql: "sql", bash: "sh", shell: "sh", markdown: "md", dart: "dart",
lua: "lua", r: "r", perl: "pl", scala: "scala", haskell: "hs", elixir: "ex",
};
function langToExt(lang) {
if (!lang) return "txt";
return EXT_MAP[lang.toLowerCase()] || "txt";
}
// --- Toast ---
function showToast(msg, url, isError = false) {
const el = document.createElement("div");
Object.assign(el.style, {
position: "fixed", bottom: "24px", right: "24px", zIndex: "99999",
background: isError ? "#d93025" : "#1a73e8", color: "#fff",
padding: "12px 20px", borderRadius: "8px", fontSize: "14px",
boxShadow: "0 4px 12px rgba(0,0,0,.3)", maxWidth: "400px",
opacity: "0", transition: "opacity .2s",
});
if (url) {
el.append(msg + " ");
const a = document.createElement("a");
a.href = url; a.target = "_blank";
Object.assign(a.style, { color: "#fff", textDecoration: "underline" });
a.textContent = url;
el.append(a);
} else {
el.textContent = msg;
}
document.body.appendChild(el);
requestAnimationFrame(() => (el.style.opacity = "1"));
setTimeout(() => {
el.style.opacity = "0";
setTimeout(() => el.remove(), 200);
}, 5000);
}
// --- Button injection ---
function createPasteButton(codeBlock) {
const btn = document.createElement("button");
btn.className = "paste-button";
btn.setAttribute("aria-label", "Paste code");
btn.dataset.tooltip = "Paste code";
Object.assign(btn.style, {
background: "none", border: "none", cursor: "pointer", color: "inherit",
display: "inline-flex", alignItems: "center", justifyContent: "center",
width: "32px", height: "32px", borderRadius: "50%", padding: "0",
});
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
icon.setAttribute("viewBox", "0 -960 960 960");
icon.setAttribute("width", "20");
icon.setAttribute("height", "20");
icon.style.fill = "currentColor";
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
// content_paste_go
const ICON_PASTE = "M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h167q11-35 43-57.5t70-22.5q40 0 71.5 22.5T594-840h166q33 0 56.5 23.5T840-760v240h-80v-240H200v560h280v80H200Zm0-120v40-560 243-3 280Zm80-40h160v-80H280v80Zm0-160h240v-80H280v80Zm0-160h240v-80H280v80Zm200 400v-97l215-216 97 98-215 215h-97Zm300-203-97-98 40-41q12-12 30-12t30 12l37 37q12 12 12 30t-12 30l-40 42ZM480-760q17 0 28.5-11.5T520-800q0-17-11.5-28.5T480-840q-17 0-28.5 11.5T440-800q0 17 11.5 28.5T480-760Z";
const ICON_CHECK = "M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z";
const ICON_ERROR = "M480-280q17 0 28.5-11.5T520-320q0-17-11.5-28.5T480-360q-17 0-28.5 11.5T440-320q0 17 11.5 28.5T480-280Zm-40-160h80v-240h-80v240Zm40 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z";
const ICON_LOADING = "M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-155.5t86-127Q252-817 325-848.5T480-880q17 0 28.5 11.5T520-840q0 17-11.5 28.5T480-800q-133 0-226.5 93.5T160-480q0 133 93.5 226.5T480-160q133 0 226.5-93.5T800-480q0-17 11.5-28.5T840-520q17 0 28.5 11.5T880-480q0 82-31.5 155t-86 127.5q-54.5 54.5-127 86T480-80Z";
path.setAttribute("d", ICON_PASTE);
icon.appendChild(path);
btn.appendChild(icon);
btn._setIcon = (name) => {
path.setAttribute("d", { paste: ICON_PASTE, check: ICON_CHECK, error: ICON_ERROR, loading: ICON_LOADING }[name] || ICON_PASTE);
icon.style.animation = name === "loading" ? "gp-spin 1s linear infinite" : "";
};
btn.addEventListener("click", async (e) => {
e.stopPropagation();
if (btn.dataset.busy) return;
btn.dataset.busy = "1";
btn._setIcon("loading");
try {
const code = codeBlock.querySelector('code[data-test-id="code-content"]')?.textContent || "";
if (!code.trim()) throw new Error("Empty code block");
const langEl = codeBlock.querySelector(".code-block-decoration > span");
const lang = langEl?.textContent?.trim() || "";
const filename = `gemini-paste.${langToExt(lang)}`;
const sid = await mcpInitialize(PASTE_URL);
const result = await mcpCallTool(PASTE_URL, sid, "paste_create", {
content: code, format: "markdown", filename,
description: lang ? `${lang} from Gemini` : "Code from Gemini",
});
const urlMatch = result.match(/https?:\/\/[^\s)"',]+/);
if (urlMatch) {
await navigator.clipboard.writeText(urlMatch[0]).catch(() => {});
showToast("Copied!", urlMatch[0]);
btn._setIcon("check");
} else {
throw new Error("No URL in response");
}
} catch (err) {
console.error("[gemini-paste]", err);
showToast(`Paste failed: ${err.message}`, null, true);
btn._setIcon("error");
}
setTimeout(() => { btn._setIcon("paste"); delete btn.dataset.busy; }, 2000);
});
return btn;
}
function processCodeBlock(block) {
if (block.hasAttribute(PROCESSED)) return;
block.setAttribute(PROCESSED, "1");
const buttons = block.querySelector(".code-block-decoration .buttons");
if (!buttons) return;
const copyBtn = buttons.querySelector('button.copy-button[aria-label="Copy code"]');
const pasteBtn = createPasteButton(block);
if (copyBtn) copyBtn.after(pasteBtn);
else buttons.appendChild(pasteBtn);
}
// --- Observer ---
function scan() {
document.querySelectorAll(`code-block:not([${PROCESSED}])`).forEach(processCodeBlock);
}
new MutationObserver(scan).observe(document.body, { childList: true, subtree: true });
scan();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment