Last active
December 1, 2025 05:04
-
-
Save kiranwayne/f462acd88a1ef4dea525c6521c2deefa to your computer and use it in GitHub Desktop.
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 Enhanced | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.4.0 | |
| // @description Customize chat content width on Google Gemini with a slider. Dark mode settings panel. | |
| // @author kiranwayne | |
| // @match https://gemini.google.com/* | |
| // @updateURL https://gist.github.com/kiranwayne/f462acd88a1ef4dea525c6521c2deefa/raw/gemini_enhanced.js | |
| // @downloadURL https://gist.github.com/kiranwayne/f462acd88a1ef4dea525c6521c2deefa/raw/gemini_enhanced.js | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @grant GM_registerMenuCommand | |
| // @grant GM_unregisterMenuCommand | |
| // @run-at document-end | |
| // ==/UserScript== | |
| (async () => { | |
| 'use strict'; | |
| // --- Configuration & Constants --- | |
| const SCRIPT_NAME = 'Gemini Enhanced'; | |
| const SCRIPT_VERSION = '1.4.0'; | |
| const SCRIPT_AUTHOR = 'kiranwayne'; | |
| const CONFIG_PREFIX = 'geminiEnhancedWidth_v1_'; | |
| const MAX_WIDTH_PX_KEY = CONFIG_PREFIX + 'maxWidthPx'; | |
| const USE_DEFAULT_WIDTH_KEY = CONFIG_PREFIX + 'useDefaultWidth'; | |
| const UI_VISIBLE_KEY = CONFIG_PREFIX + 'uiVisible'; | |
| const WIDTH_STYLE_ID = 'gemini-enhanced-width-style'; | |
| const GLOBAL_STYLE_ID = 'gemini-enhanced-global-style'; | |
| const SETTINGS_PANEL_ID = 'gemini-userscript-settings-panel'; | |
| // Targets both the conversation container and the user bubbles | |
| const WIDTH_TARGET_SELECTOR = '.conversation-container, .conversation-container user-query'; | |
| // --- Constraints --- | |
| const SCRIPT_DEFAULT_CUSTOM_WIDTH_PX = 1380; | |
| const MIN_WIDTH_PX = 800; | |
| const MAX_WIDTH_PX = 2000; | |
| const STEP_WIDTH_PX = 10; | |
| let config = { | |
| maxWidthPx: SCRIPT_DEFAULT_CUSTOM_WIDTH_PX, | |
| useDefaultWidth: true, | |
| uiVisible: false, | |
| }; | |
| let settingsPanel = null, widthSlider = null, widthLabel = null, widthInput = null; | |
| let defaultWidthCheckbox = null; | |
| let menuCommandId_ToggleUI = null; | |
| // --- Helper Functions --- | |
| async function loadSettings() { | |
| config.useDefaultWidth = await GM_getValue(USE_DEFAULT_WIDTH_KEY, true); | |
| config.maxWidthPx = await GM_getValue(MAX_WIDTH_PX_KEY, SCRIPT_DEFAULT_CUSTOM_WIDTH_PX); | |
| config.maxWidthPx = Math.max(MIN_WIDTH_PX, Math.min(MAX_WIDTH_PX, config.maxWidthPx)); | |
| config.uiVisible = await GM_getValue(UI_VISIBLE_KEY, false); | |
| } | |
| async function saveSetting(key, value) { | |
| if (key === MAX_WIDTH_PX_KEY) { | |
| const numValue = parseInt(value, 10); | |
| if (!isNaN(numValue)) { | |
| const clampedValue = Math.max(MIN_WIDTH_PX, Math.min(MAX_WIDTH_PX, numValue)); | |
| await GM_setValue(key, clampedValue); | |
| config.maxWidthPx = clampedValue; | |
| } | |
| } else { | |
| await GM_setValue(key, value); | |
| if (key === USE_DEFAULT_WIDTH_KEY) config.useDefaultWidth = value; | |
| else if (key === UI_VISIBLE_KEY) config.uiVisible = value; | |
| } | |
| } | |
| // --- Style Generation Functions --- | |
| function getCustomWidthConstraintCss() { | |
| if (config.useDefaultWidth) return ''; | |
| return `${WIDTH_TARGET_SELECTOR} { max-width: ${config.maxWidthPx}px !important; width: 100% !important; }`; | |
| } | |
| function getGlobalPanelCss() { | |
| return ` | |
| #${SETTINGS_PANEL_ID} { | |
| font-family: "Google Sans", Roboto, Helvetica, Arial, sans-serif; | |
| font-size: 13px; | |
| line-height: 1.4; | |
| position: fixed; | |
| top: 70px; /* Moved down to avoid header overlap */ | |
| right: 20px; | |
| z-index: 2147483647; /* Max Z-Index to ensure visibility */ | |
| background-color: #1e1f20; | |
| color: #e3e3e3; | |
| border: 1px solid #444746; | |
| border-radius: 8px; | |
| padding: 14px; | |
| box-shadow: 0 4px 24px rgba(0,0,0,0.8); | |
| min-width: 250px; | |
| display: flex !important; | |
| flex-direction: column; | |
| gap: 4px; | |
| opacity: 1 !important; | |
| visibility: visible !important; | |
| } | |
| #${SETTINGS_PANEL_ID} h4 { | |
| margin: 0 0 8px 0; | |
| font-size: 15px; | |
| font-weight: 600; | |
| color: #e3e3e3; | |
| padding-bottom: 6px; | |
| border-bottom: 1px solid #444746; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| #${SETTINGS_PANEL_ID} .close-btn { | |
| cursor: pointer; | |
| font-size: 20px; | |
| color: #8e918f; | |
| line-height: 1; | |
| font-weight: normal; | |
| } | |
| #${SETTINGS_PANEL_ID} .close-btn:hover { color: #fff; } | |
| #${SETTINGS_PANEL_ID} .meta-info { | |
| margin: 0; | |
| font-size: 11px; | |
| color: #c4c7c5; | |
| opacity: 0.8; | |
| line-height: 1.3; | |
| } | |
| #${SETTINGS_PANEL_ID} .control-group { margin-top: 10px; } | |
| #${SETTINGS_PANEL_ID} label { | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| color: #e3e3e3; | |
| margin-bottom: 8px; | |
| } | |
| #${SETTINGS_PANEL_ID} input[type="checkbox"] { | |
| margin-right: 8px; | |
| accent-color: #8ab4f8; | |
| width: 14px; height: 14px; | |
| } | |
| #${SETTINGS_PANEL_ID} .width-controls-wrapper { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| #${SETTINGS_PANEL_ID} input[type="range"] { | |
| flex-grow: 1; | |
| accent-color: #8ab4f8; | |
| background: #444746; | |
| height: 4px; | |
| border-radius: 2px; | |
| cursor: pointer; | |
| } | |
| #${SETTINGS_PANEL_ID} input[type="number"] { | |
| width: 55px; | |
| padding: 4px 6px; | |
| background-color: #2a2b2d; | |
| color: #e3e3e3; | |
| border: 1px solid #444746; | |
| border-radius: 6px; | |
| text-align: left; | |
| font-family: monospace; | |
| font-size: 12px; | |
| } | |
| #${SETTINGS_PANEL_ID} input[type="number"]:focus { outline: 2px solid #8ab4f8; border-color: transparent; } | |
| #${SETTINGS_PANEL_ID} .width-value-label { | |
| min-width: 45px; | |
| text-align: left; | |
| font-family: monospace; | |
| color: #e3e3e3; | |
| font-size: 12px; | |
| } | |
| #${SETTINGS_PANEL_ID} input:disabled, | |
| #${SETTINGS_PANEL_ID} .width-value-label.disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| `; | |
| } | |
| // --- Style Injection --- | |
| function injectOrUpdateStyle(styleId, cssContent) { | |
| let style = document.getElementById(styleId); | |
| if (cssContent && cssContent.trim() !== '') { | |
| if (!style) { | |
| style = document.createElement('style'); | |
| style.id = styleId; | |
| style.textContent = cssContent; | |
| // Append to documentElement (html) if head is missing or problematic, otherwise head | |
| (document.head || document.documentElement).appendChild(style); | |
| } else if (style.textContent !== cssContent) { | |
| style.textContent = cssContent; | |
| } | |
| } else { | |
| if (style) style.remove(); | |
| } | |
| } | |
| function applyStyles() { | |
| injectOrUpdateStyle(GLOBAL_STYLE_ID, getGlobalPanelCss()); | |
| injectOrUpdateStyle(WIDTH_STYLE_ID, getCustomWidthConstraintCss()); | |
| } | |
| // --- UI Logic --- | |
| function updateUIState() { | |
| if (!settingsPanel) return; | |
| defaultWidthCheckbox.checked = config.useDefaultWidth; | |
| const isCustomWidthEnabled = !config.useDefaultWidth; | |
| [widthSlider, widthInput].forEach(el => el.disabled = !isCustomWidthEnabled); | |
| const opacityValue = isCustomWidthEnabled ? 1 : 0.5; | |
| widthSlider.style.opacity = opacityValue; | |
| widthInput.style.opacity = opacityValue; | |
| widthLabel.style.opacity = opacityValue; | |
| widthSlider.value = config.maxWidthPx; | |
| widthInput.value = config.maxWidthPx; | |
| widthLabel.textContent = `${config.maxWidthPx}px`; | |
| } | |
| async function closePanel() { | |
| await saveSetting(UI_VISIBLE_KEY, false); | |
| removeSettingsUI(); | |
| updateTampermonkeyMenu(); | |
| } | |
| function removeSettingsUI() { | |
| settingsPanel = document.getElementById(SETTINGS_PANEL_ID); | |
| if (settingsPanel) settingsPanel.remove(); | |
| settingsPanel = null; | |
| } | |
| function createSettingsUI() { | |
| if (document.getElementById(SETTINGS_PANEL_ID)) return; | |
| if (!config.uiVisible) return; | |
| // Use standard element creation to avoid TrustedHTML issues | |
| settingsPanel = document.createElement('div'); | |
| settingsPanel.id = SETTINGS_PANEL_ID; | |
| // Header | |
| const header = document.createElement('h4'); | |
| const titleSpan = document.createElement('span'); | |
| titleSpan.textContent = SCRIPT_NAME; | |
| const closeBtn = document.createElement('span'); | |
| closeBtn.className = 'close-btn'; | |
| closeBtn.textContent = '×'; | |
| closeBtn.onclick = closePanel; | |
| header.appendChild(titleSpan); | |
| header.appendChild(closeBtn); | |
| settingsPanel.appendChild(header); | |
| // Meta Info | |
| const versionP = document.createElement('p'); | |
| versionP.className = 'meta-info'; | |
| versionP.textContent = `Version: ${SCRIPT_VERSION}`; | |
| settingsPanel.appendChild(versionP); | |
| const authorP = document.createElement('p'); | |
| authorP.className = 'meta-info'; | |
| authorP.textContent = `Author: ${SCRIPT_AUTHOR}`; | |
| settingsPanel.appendChild(authorP); | |
| // Controls | |
| const controlContainer = document.createElement('div'); | |
| controlContainer.className = 'control-group'; | |
| // Checkbox | |
| const checkboxWrapper = document.createElement('div'); | |
| defaultWidthCheckbox = document.createElement('input'); | |
| defaultWidthCheckbox.type = 'checkbox'; | |
| defaultWidthCheckbox.id = 'gemini-width-toggle'; | |
| const checkboxLabel = document.createElement('label'); | |
| checkboxLabel.htmlFor = 'gemini-width-toggle'; | |
| checkboxLabel.appendChild(defaultWidthCheckbox); | |
| checkboxLabel.appendChild(document.createTextNode(' Use Gemini Default Width')); | |
| checkboxWrapper.appendChild(checkboxLabel); | |
| // Slider Row | |
| const sliderWrapper = document.createElement('div'); | |
| sliderWrapper.className = 'width-controls-wrapper'; | |
| widthLabel = document.createElement('span'); | |
| widthLabel.className = 'width-value-label'; | |
| widthSlider = document.createElement('input'); | |
| widthSlider.type = 'range'; | |
| widthSlider.min = MIN_WIDTH_PX; | |
| widthSlider.max = MAX_WIDTH_PX; | |
| widthSlider.step = STEP_WIDTH_PX; | |
| widthInput = document.createElement('input'); | |
| widthInput.type = 'number'; | |
| widthInput.min = MIN_WIDTH_PX; | |
| widthInput.max = MAX_WIDTH_PX; | |
| widthInput.step = STEP_WIDTH_PX; | |
| sliderWrapper.appendChild(widthLabel); | |
| sliderWrapper.appendChild(widthSlider); | |
| sliderWrapper.appendChild(widthInput); | |
| controlContainer.appendChild(checkboxWrapper); | |
| controlContainer.appendChild(sliderWrapper); | |
| settingsPanel.appendChild(controlContainer); | |
| document.body.appendChild(settingsPanel); | |
| // --- Event Listeners --- | |
| defaultWidthCheckbox.addEventListener('change', async (e) => { | |
| await saveSetting(USE_DEFAULT_WIDTH_KEY, e.target.checked); | |
| applyStyles(); | |
| updateUIState(); | |
| }); | |
| const applyWidthChange = (val) => { | |
| config.maxWidthPx = parseInt(val, 10); | |
| widthLabel.textContent = `${config.maxWidthPx}px`; | |
| if (!config.useDefaultWidth) injectOrUpdateStyle(WIDTH_STYLE_ID, getCustomWidthConstraintCss()); | |
| }; | |
| // Slider (Instant) | |
| widthSlider.addEventListener('input', (e) => { | |
| widthInput.value = e.target.value; | |
| applyWidthChange(e.target.value); | |
| }); | |
| widthSlider.addEventListener('change', async (e) => { | |
| await saveSetting(MAX_WIDTH_PX_KEY, e.target.value); | |
| }); | |
| // Input (Commit on change) | |
| const commitInputValue = async () => { | |
| let val = parseInt(widthInput.value, 10); | |
| if (isNaN(val)) { | |
| widthInput.value = config.maxWidthPx; | |
| return; | |
| } | |
| val = Math.max(MIN_WIDTH_PX, Math.min(MAX_WIDTH_PX, val)); | |
| widthInput.value = val; | |
| widthSlider.value = val; | |
| await saveSetting(MAX_WIDTH_PX_KEY, val); | |
| applyWidthChange(val); | |
| }; | |
| widthInput.addEventListener('change', commitInputValue); | |
| widthInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') { | |
| widthInput.blur(); | |
| } | |
| }); | |
| updateUIState(); | |
| } | |
| function updateTampermonkeyMenu() { | |
| if (menuCommandId_ToggleUI !== null) { | |
| GM_unregisterMenuCommand(menuCommandId_ToggleUI); | |
| } | |
| const label = config.uiVisible ? 'Hide Gemini Width Settings' : 'Show Gemini Width Settings'; | |
| menuCommandId_ToggleUI = GM_registerMenuCommand(label, async () => { | |
| config.uiVisible = !config.uiVisible; | |
| await saveSetting(UI_VISIBLE_KEY, config.uiVisible); | |
| if (config.uiVisible) createSettingsUI(); | |
| else removeSettingsUI(); | |
| updateTampermonkeyMenu(); | |
| }); | |
| } | |
| // --- Initialization --- | |
| await loadSettings(); | |
| applyStyles(); | |
| // Slight delay to ensure body is ready for panel | |
| if (config.uiVisible) { | |
| setTimeout(createSettingsUI, 500); | |
| } | |
| updateTampermonkeyMenu(); | |
| // Re-apply styles if page dynamically clears head | |
| const headObserver = new MutationObserver(() => { | |
| if (!document.getElementById(WIDTH_STYLE_ID) || !document.getElementById(GLOBAL_STYLE_ID)) { | |
| applyStyles(); | |
| } | |
| }); | |
| headObserver.observe(document.head || document.documentElement, { childList: true }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment