// ==UserScript== // @name Claude – wider chat column // @namespace local.claude.wide-column // @version 1.1.0 // @description Widen or narrow the chat column on claude.ai. Floating controls, keyboard shortcuts and saved width. Reset restores the site's own stylesheet. // @author Francisco Ribera // @license MIT // @homepageURL https://github.com/Ademord/claude-wide-column // @supportURL https://github.com/Ademord/claude-wide-column/issues // @downloadURL https://raw.githubusercontent.com/Ademord/claude-wide-column/main/claude-wide-column.user.js // @updateURL https://raw.githubusercontent.com/Ademord/claude-wide-column/main/claude-wide-column.user.js // @match https://claude.ai/* // @run-at document-start // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // ==/UserScript== (() => { 'use strict'; // ---------------------------------------------------------------- config const STEP = 80; // px per click / key press. Shift-click = 3× step. const MIN_WIDTH = 600; // px const KEYS = { // e.code values; edit to taste wider: { ctrl: true, alt: true, code: 'ArrowRight' }, narrow: { ctrl: true, alt: true, code: 'ArrowLeft' }, reset: { ctrl: true, alt: true, code: 'ArrowDown' }, }; // Both cap the column: chat-column wraps transcript + composer (max-w-[50.5rem]), // transcript-list has its own max-w-3xl (768px). The escaped class is a fallback // in case the data-testid attributes disappear in a redesign. const TARGETS = [ '[data-testid="chat-column"]', '[data-testid="transcript-list"]', '.max-w-\\[50\\.5rem\\]', ].join(','); // ----------------------------------------------------------------- state const K_WIDTH = 'width'; const K_PANEL = 'panelVisible'; const clamp = px => Math.min(Math.max(Math.round(px), Math.min(MIN_WIDTH, window.innerWidth)), window.innerWidth); const savedWidth = GM_getValue(K_WIDTH, null); let width = typeof savedWidth === 'number' && Number.isFinite(savedWidth) && savedWidth > 0 ? clamp(savedWidth) : null; let panelVisible = GM_getValue(K_PANEL, true) !== false; const widthStyle = document.createElement('style'); widthStyle.id = 'cw-width'; (document.head || document.documentElement).appendChild(widthStyle); const panelStyle = document.createElement('style'); panelStyle.id = 'cw-panel-style'; panelStyle.textContent = ` #cw-panel{position:fixed;right:16px;bottom:16px;z-index:2147483647;display:flex;align-items:center;gap:2px; padding:4px 6px;border-radius:999px;font:12px/1 system-ui,sans-serif; background:var(--cds-page-bg,Canvas);color:inherit;border:1px solid var(--cds-border,rgba(127,127,127,.35)); box-shadow:0 2px 10px rgba(0,0,0,.18);opacity:.9;transition:opacity .15s;user-select:none} #cw-panel:hover,#cw-panel:focus-within{opacity:1} #cw-panel button{all:unset;cursor:pointer;min-width:22px;height:22px;padding:0 7px;border-radius:999px; text-align:center;line-height:22px;font:inherit;color:inherit} #cw-panel button:hover{background:var(--cds-fill-ghost-hover,rgba(127,127,127,.22))} #cw-panel button:focus-visible{outline:2px solid currentColor;outline-offset:2px} #cw-panel button[disabled]{opacity:.4;cursor:default;background:none} #cw-label{min-width:52px;text-align:center;font-variant-numeric:tabular-nums;opacity:.85} #cw-panel[hidden]{display:none} `; (document.head || document.documentElement).appendChild(panelStyle); // ---------------------------------------------------------------- logic let stockCache = null; function stockPx() { // Only measure while no override is active, otherwise we'd read our own value back. if (width == null) { const el = document.querySelector('[data-testid="chat-column"]'); const v = el ? parseFloat(getComputedStyle(el).maxWidth) : NaN; if (Number.isFinite(v)) stockCache = Math.round(v); } return stockCache ?? 808; } const currentPx = () => (width == null ? stockPx() : width); function apply() { if (width == null) { widthStyle.textContent = ''; // site CSS wins again GM_deleteValue(K_WIDTH); } else { widthStyle.textContent = `${TARGETS}{max-width:${width}px !important;}`; GM_setValue(K_WIDTH, width); } updatePanel(); } function setWidth(px) { width = clamp(px); apply(); } const wider = (m = 1) => setWidth(currentPx() + STEP * m); const narrow = (m = 1) => setWidth(currentPx() - STEP * m); const reset = () => { width = null; apply(); }; // ---------------------------------------------------------------- panel let panel, label, resetBtn; const fmtKey = k => [k.ctrl && 'Ctrl', k.alt && 'Alt', k.shift && 'Shift', k.code.replace('Arrow', '')] .filter(Boolean).join('+'); function buildPanel() { if (!document.body || panel?.isConnected) return; if (panel) { document.body.appendChild(panel); return; } panel = document.createElement('div'); panel.id = 'cw-panel'; panel.setAttribute('role', 'group'); panel.setAttribute('aria-label', 'Chat column width'); panel.innerHTML = ` `; label = panel.querySelector('#cw-label'); resetBtn = panel.querySelector('[data-act="reset"]'); panel.addEventListener('click', e => { const act = e.target.closest('button')?.dataset.act; const m = e.shiftKey ? 3 : 1; if (act === 'wider') wider(m); else if (act === 'narrow') narrow(m); else if (act === 'reset') reset(); }); document.body.appendChild(panel); updatePanel(); } function updatePanel() { if (!panel) return; panel.hidden = !panelVisible; label.textContent = width == null ? `${stockPx()}px` : `${width}px`; label.title = width == null ? 'stock width' : `stock is ${stockPx()}px`; resetBtn.disabled = width == null; } // ------------------------------------------------------------- keyboard const match = (e, k) => e.code === k.code && e.ctrlKey === !!k.ctrl && e.altKey === !!k.alt && e.shiftKey === !!k.shift && !e.metaKey; document.addEventListener('keydown', e => { if (match(e, KEYS.wider)) wider(); else if (match(e, KEYS.narrow)) narrow(); else if (match(e, KEYS.reset)) reset(); else return; e.preventDefault(); e.stopPropagation(); }, true); // ------------------------------------------------------------------ menu GM_registerMenuCommand('Toggle width panel', () => { panelVisible = !panelVisible; GM_setValue(K_PANEL, panelVisible); updatePanel(); }); GM_registerMenuCommand('Reset column width', reset); // ------------------------------------------------------------------ boot apply(); // width override, before first paint if (document.body) buildPanel(); else document.addEventListener('DOMContentLoaded', buildPanel, { once: true }); window.addEventListener('resize', () => { if (width != null && width > window.innerWidth) setWidth(width); }); // CSS automatically applies to replacement chat containers. Restore the pill // and styles as well if an app navigation replaces a larger subtree. let stockElement = document.querySelector('[data-testid="chat-column"]'); new MutationObserver(() => { if (!window.document) return; // Ignore queued records after document teardown. if (!widthStyle.isConnected) (document.head || document.documentElement).appendChild(widthStyle); if (!panelStyle.isConnected) (document.head || document.documentElement).appendChild(panelStyle); buildPanel(); const next = document.querySelector('[data-testid="chat-column"]'); if (next !== stockElement) { stockElement = next; stockCache = null; if (width == null) updatePanel(); } }).observe(document.documentElement, { childList: true, subtree: true }); })();