Skip to content

Instantly share code, notes, and snippets.

@kiranwayne
Last active November 14, 2025 16:56
Show Gist options
  • Select an option

  • Save kiranwayne/9b676d5973aad64fd4711da1f110ecac to your computer and use it in GitHub Desktop.

Select an option

Save kiranwayne/9b676d5973aad64fd4711da1f110ecac to your computer and use it in GitHub Desktop.
Adds a movable panel with metadata to adjust content width, justify text, or use default width.
// ==UserScript==
// @name AI Studio Enhanced
// @namespace https://gist.github.com/kiranwayne
// @version 0.5.4
// @description Adds a movable panel with metadata to adjust content width, justify text, or use default width.
// @author kiranwayne
// @match https://aistudio.google.com/*
// @updateURL https://gist.githubusercontent.com/kiranwayne/9b676d5973aad64fd4711da1f110ecac/raw/ai_studio_enhanced.js
// @downloadURL https://gist.githubusercontent.com/kiranwayne/9b676d5973aad64fd4711da1f110ecac/raw/ai_studio_enhanced.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @run-at document-idle
// ==/UserScript==
(async () => {
'use strict';
// --- Configuration & Constants ---
const SCRIPT_NAME = 'AI Studio Enhanced';
const SCRIPT_VERSION = '0.5.4'; // <-- Changed: Version updated
const SCRIPT_AUTHOR = 'kiranwayne';
const CONFIG_PREFIX = 'aiStudioEnhanced_v2_';
const PANEL_VISIBLE_KEY = CONFIG_PREFIX + 'panelVisible';
const PANEL_COLLAPSED_KEY = CONFIG_PREFIX + 'panelCollapsed';
const PANEL_POS_TOP_KEY = CONFIG_PREFIX + 'posTop';
const PANEL_POS_LEFT_KEY = CONFIG_PREFIX + 'posLeft';
const STYLE_WIDTH_KEY = CONFIG_PREFIX + 'styleWidth';
const STYLE_JUSTIFY_KEY = CONFIG_PREFIX + 'styleJustify';
const STYLE_USE_DEFAULT_WIDTH_KEY = CONFIG_PREFIX + 'styleUseDefaultWidth';
const DYNAMIC_STYLE_ID = 'ai-studio-enhanced-dynamic-style';
const PANEL_STYLE_ID = 'ai-studio-enhanced-panel-style';
const CONTROLS_PANEL_ID = 'ai-studio-enhanced-panel';
const HEADER_CLASS = 'ai-studio-enhanced-header';
const TURN_SELECTOR = 'ms-chat-turn';
const TEXT_CHUNK_SELECTOR = 'ms-text-chunk';
const CHAT_SESSION_SELECTOR = '.chat-session-content'; // <-- Added: New selector for the parent container
// --- State Variables ---
let panelConfig = {
visible: true,
collapsed: false,
posTop: '80px',
posLeft: 'calc(100vw - 320px)'
};
let styleConfig = {
width: 1500,
justify: false,
useDefault: false
};
const allStyleRoots = new Set();
let controlsPanel = null;
let menuCommandId = null;
let observer = null;
// Dragging state
let isDragging = false;
let initialMouseX = 0, initialMouseY = 0;
let initialPanelX = 0, initialPanelY = 0;
// --- Helper Functions ---
async function loadSettings() {
panelConfig.visible = await GM_getValue(PANEL_VISIBLE_KEY, true);
panelConfig.collapsed = await GM_getValue(PANEL_COLLAPSED_KEY, false);
panelConfig.posTop = await GM_getValue(PANEL_POS_TOP_KEY, '80px');
panelConfig.posLeft = await GM_getValue(PANEL_POS_LEFT_KEY, `calc(100vw - 320px)`);
styleConfig.width = await GM_getValue(STYLE_WIDTH_KEY, 1500);
styleConfig.justify = await GM_getValue(STYLE_JUSTIFY_KEY, false);
styleConfig.useDefault = await GM_getValue(STYLE_USE_DEFAULT_WIDTH_KEY, false);
}
async function saveSetting(key, value) {
await GM_setValue(key, value);
}
// --- Panel Dragging Logic ---
function handleMouseDown(e) {
if (e.button !== 0 || e.target.closest('button, input, label')) return;
if (!controlsPanel) return;
isDragging = true;
initialMouseX = e.clientX;
initialMouseY = e.clientY;
const rect = controlsPanel.getBoundingClientRect();
initialPanelX = rect.left;
initialPanelY = rect.top;
controlsPanel.classList.add('dragging');
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
e.preventDefault();
}
function handleMouseMove(e) {
if (!isDragging || !controlsPanel) return;
const deltaX = e.clientX - initialMouseX;
const deltaY = e.clientY - initialMouseY;
let newPanelX = initialPanelX + deltaX;
let newPanelY = initialPanelY + deltaY;
newPanelX = Math.max(0, Math.min(newPanelX, window.innerWidth - controlsPanel.offsetWidth - 5));
newPanelY = Math.max(0, Math.min(newPanelY, window.innerHeight - controlsPanel.offsetHeight - 5));
controlsPanel.style.left = `${newPanelX}px`;
controlsPanel.style.top = `${newPanelY}px`;
}
function handleMouseUp() {
if (!isDragging || !controlsPanel) return;
isDragging = false;
controlsPanel.classList.remove('dragging');
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
saveSetting(PANEL_POS_TOP_KEY, controlsPanel.style.top);
saveSetting(PANEL_POS_LEFT_KEY, controlsPanel.style.left);
}
// --- CSS Generation ---
function getPanelCss() {
return `
#${CONTROLS_PANEL_ID} { position: fixed; z-index: 9999; background: rgba(30, 30, 35, 0.9); color: #E0E0E0; border: 1px solid #555; border-radius: 8px; padding: 12px; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.4); min-width: 300px; font-size: 14px; backdrop-filter: blur(5px); transition: opacity 0.3s ease-out; user-select: none; -webkit-user-select: none; }
#${CONTROLS_PANEL_ID}.hidden { opacity: 0; pointer-events: none; }
#${CONTROLS_PANEL_ID} .${HEADER_CLASS} { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; cursor: move; }
#${CONTROLS_PANEL_ID} h5 { margin: 0; font-size: 1.1em; font-weight: bold; color: #FFFFFF; }
#${CONTROLS_PANEL_ID} .collapse-btn { background: none; border: none; color: #ccc; cursor: pointer; padding: 2px 5px; font-size: 1.3em; line-height: 1; border-radius: 4px; transition: background-color 0.2s; }
#${CONTROLS_PANEL_ID} .collapse-btn:hover { background-color: #4a4b54; }
#${CONTROLS_PANEL_ID}[data-collapsed="true"] .panel-body { display: none; }
.metadata-container { padding: 0 4px; }
.metadata-text { margin: 4px 0; font-size: 0.9em; color: #ccc; }
.separator { border: none; border-top: 1px solid #555; margin: 12px 0; }
#${CONTROLS_PANEL_ID} .control-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; gap: 8px; }
#${CONTROLS_PANEL_ID} .control-row label { flex-shrink: 0; }
#${CONTROLS_PANEL_ID} .control-row input[type="range"] { flex-grow: 1; margin: 0 5px; }
#${CONTROLS_PANEL_ID} .control-row input[type="number"] { width: 65px; background: #222; color: #ddd; border: 1px solid #555; border-radius: 4px; padding: 4px 5px; text-align: center; }
#${CONTROLS_PANEL_ID} .control-row input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
#${CONTROLS_PANEL_ID} .control-row input:disabled { background: #444; color: #888; cursor: not-allowed; }
#${CONTROLS_PANEL_ID} .width-display { font-family: monospace; min-width: 45px; text-align: left; }
#${CONTROLS_PANEL_ID} .check-label { display: flex; align-items: center; cursor: pointer; width: 100%; }
#${CONTROLS_PANEL_ID} .check-label input { margin-right: 8px; }
#${CONTROLS_PANEL_ID}.dragging { opacity: 0.85; border: 1px dashed #aaa; }
`;
}
// --- Changed: Added new rule to remove parent container width limit ---
function generateDynamicCss() {
let css = '';
if (!styleConfig.useDefault) {
css += `
/* Remove the max-width constraint from the main content area */
${CHAT_SESSION_SELECTOR} {
max-width: none !important;
}
/* Apply custom width to individual chat turns */
${TURN_SELECTOR} {
max-width: ${styleConfig.width}px !important;
margin-left: auto !important;
margin-right: auto !important;
}
`;
}
if (styleConfig.justify) {
css += `
${TURN_SELECTOR} ${TEXT_CHUNK_SELECTOR} {
text-align: justify !important;
}
`;
}
return css;
}
// --- End of Changes ---
// --- Style Injection & Shadow DOM Handling ---
function injectStyle(targetNode, styleId, cssContent) {
if (!targetNode || !targetNode.querySelector) return;
let styleElement = targetNode.querySelector('#' + styleId);
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = styleId;
styleElement.textContent = cssContent;
targetNode.appendChild(styleElement);
} else if (styleElement.textContent !== cssContent) {
styleElement.textContent = cssContent;
}
}
function findAndApplyStylesToAllRoots(styleId, cssGenerator) {
const css = cssGenerator();
const rootsToStyle = [document.head || document.documentElement, ...allStyleRoots];
rootsToStyle.forEach(root => {
if (root) injectStyle(root, styleId, css);
});
}
// --- Core Logic ---
function updateDynamicStyles() {
findAndApplyStylesToAllRoots(DYNAMIC_STYLE_ID, generateDynamicCss);
}
// --- UI Functions ---
function removeControlsUI() {
if (controlsPanel) {
controlsPanel.remove();
controlsPanel = null;
}
}
function updatePanelUIState() {
if (!controlsPanel) return;
controlsPanel.setAttribute('data-collapsed', panelConfig.collapsed);
const collapseBtn = controlsPanel.querySelector('.collapse-btn');
if (collapseBtn) {
collapseBtn.textContent = panelConfig.collapsed ? '⊕' : '⊖';
}
controlsPanel.classList.toggle('hidden', !panelConfig.visible);
}
function createControlsUI() {
if (document.getElementById(CONTROLS_PANEL_ID)) return;
if (!panelConfig.visible) return;
controlsPanel = document.createElement('div');
controlsPanel.id = CONTROLS_PANEL_ID;
controlsPanel.style.top = panelConfig.posTop;
controlsPanel.style.left = panelConfig.posLeft;
// --- Header ---
const header = document.createElement('div');
header.className = HEADER_CLASS;
header.addEventListener('mousedown', handleMouseDown);
const title = document.createElement('h5');
title.textContent = SCRIPT_NAME;
const collapseBtn = document.createElement('button');
collapseBtn.className = 'collapse-btn';
collapseBtn.type = 'button';
collapseBtn.addEventListener('click', () => {
panelConfig.collapsed = !panelConfig.collapsed;
saveSetting(PANEL_COLLAPSED_KEY, panelConfig.collapsed);
updatePanelUIState();
});
header.appendChild(title);
header.appendChild(collapseBtn);
controlsPanel.appendChild(header);
// This div will hold everything that collapses
const panelBody = document.createElement('div');
panelBody.className = 'panel-body';
// --- Metadata ---
const metadataContainer = document.createElement('div');
metadataContainer.className = 'metadata-container';
const versionP = document.createElement('p');
versionP.className = 'metadata-text';
versionP.textContent = `Version: ${SCRIPT_VERSION}`;
const authorP = document.createElement('p');
authorP.className = 'metadata-text';
authorP.textContent = `Author: ${SCRIPT_AUTHOR}`;
metadataContainer.appendChild(versionP);
metadataContainer.appendChild(authorP);
panelBody.appendChild(metadataContainer);
panelBody.appendChild(Object.assign(document.createElement('hr'), { className: 'separator' }));
// --- Controls ---
const controlsContainer = document.createElement('div');
controlsContainer.className = 'controls-container';
const defaultWidthRow = document.createElement('div');
defaultWidthRow.className = 'control-row';
const defaultWidthLabel = document.createElement('label');
defaultWidthLabel.className = 'check-label';
const defaultWidthCheckbox = document.createElement('input');
defaultWidthCheckbox.type = 'checkbox';
defaultWidthCheckbox.checked = styleConfig.useDefault;
defaultWidthLabel.append(defaultWidthCheckbox, 'Use Default Width');
defaultWidthRow.appendChild(defaultWidthLabel);
controlsContainer.appendChild(defaultWidthRow);
const widthRow = document.createElement('div');
widthRow.className = 'control-row';
const widthDisplay = document.createElement('span');
widthDisplay.className = 'width-display';
widthDisplay.textContent = `${styleConfig.width}px`;
const widthSlider = document.createElement('input');
widthSlider.type = 'range';
widthSlider.min = 800;
widthSlider.max = 2000;
widthSlider.step = 10;
widthSlider.value = styleConfig.width;
const widthInput = document.createElement('input');
widthInput.type = 'number';
widthInput.min = 800;
widthInput.max = 2000;
widthInput.step = 10;
widthInput.value = styleConfig.width;
widthRow.append(widthDisplay, widthSlider, widthInput);
controlsContainer.appendChild(widthRow);
const syncWidth = (newValue) => {
const val = Math.max(parseInt(widthSlider.min), Math.min(parseInt(widthSlider.max), parseInt(newValue) || 0));
styleConfig.width = val;
widthSlider.value = val;
widthInput.value = val;
widthDisplay.textContent = `${val}px`;
updateDynamicStyles();
};
widthSlider.addEventListener('input', () => syncWidth(widthSlider.value));
widthInput.addEventListener('change', () => syncWidth(widthInput.value));
widthSlider.addEventListener('change', () => saveSetting(STYLE_WIDTH_KEY, styleConfig.width));
widthInput.addEventListener('change', () => saveSetting(STYLE_WIDTH_KEY, styleConfig.width));
controlsContainer.appendChild(Object.assign(document.createElement('hr'), { className: 'separator' }));
const justifyRow = document.createElement('div');
justifyRow.className = 'control-row';
const justifyLabel = document.createElement('label');
justifyLabel.className = 'check-label';
const justifyCheckbox = document.createElement('input');
justifyCheckbox.type = 'checkbox';
justifyCheckbox.checked = styleConfig.justify;
justifyCheckbox.addEventListener('change', () => {
styleConfig.justify = justifyCheckbox.checked;
saveSetting(STYLE_JUSTIFY_KEY, styleConfig.justify);
updateDynamicStyles();
});
justifyLabel.append(justifyCheckbox, 'Justify Text');
justifyRow.appendChild(justifyLabel);
controlsContainer.appendChild(justifyRow);
panelBody.appendChild(controlsContainer);
const updateWidthControlsState = () => {
const disabled = styleConfig.useDefault;
widthSlider.disabled = disabled;
widthInput.disabled = disabled;
};
defaultWidthCheckbox.addEventListener('change', () => {
styleConfig.useDefault = defaultWidthCheckbox.checked;
saveSetting(STYLE_USE_DEFAULT_WIDTH_KEY, styleConfig.useDefault);
updateWidthControlsState();
updateDynamicStyles();
});
controlsPanel.appendChild(panelBody);
document.body.appendChild(controlsPanel);
updatePanelUIState();
updateWidthControlsState();
}
// --- Tampermonkey Menu ---
function updateTampermonkeyMenu() {
if (menuCommandId !== null) GM_unregisterMenuCommand(menuCommandId);
const label = panelConfig.visible ? 'Hide Layout Controls' : 'Show Layout Controls';
menuCommandId = GM_registerMenuCommand(label, () => {
panelConfig.visible = !panelConfig.visible;
saveSetting(PANEL_VISIBLE_KEY, panelConfig.visible);
if (panelConfig.visible) createControlsUI();
else removeControlsUI();
updateTampermonkeyMenu();
});
}
// --- Initialization & Observer ---
function initializeWhenReady() {
findAndApplyStylesToAllRoots(PANEL_STYLE_ID, getPanelCss);
updateDynamicStyles();
if (panelConfig.visible) createControlsUI();
updateTampermonkeyMenu();
startObserver();
}
function startObserver() {
const styleNewRoot = (root) => {
if (root && !allStyleRoots.has(root)) {
allStyleRoots.add(root);
injectStyle(root, PANEL_STYLE_ID, getPanelCss());
injectStyle(root, DYNAMIC_STYLE_ID, generateDynamicCss());
}
};
observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node instanceof Element) {
if (node.shadowRoot) styleNewRoot(node.shadowRoot);
node.querySelectorAll('*').forEach(el => {
if (el.shadowRoot) styleNewRoot(el.shadowRoot);
});
}
});
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
console.log(`[${SCRIPT_NAME}] v${SCRIPT_VERSION} starting...`);
await loadSettings();
if (document.readyState === 'complete' || document.readyState === 'interactive') {
initializeWhenReady();
} else {
document.addEventListener('DOMContentLoaded', initializeWhenReady, { once: true });
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment