// ==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 });
})();