Created
February 16, 2026 04:52
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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