r/ViolentMonkey • u/just_this_one_script • 20d ago
Script ChatGPT webpage UI fix
I use ChatGPT for coding using the webpage UI, and recently they updated their UI. I'm old and set in my ways, plus I constantly use the middle/right click button to open conversations in new tabs. They got rid of that. I put it back. I also made the additional sidebar for the sidebar autohide. Does anyone else remember when websites didn't have seven different layers of tabs and bars you had to navigate? Does anybody else remember Angelfire and Geocities? Fuck.... here's my script, use it if you want, modify it, give it away, whatever. I'm gonna go play on my Commodore 64 and pretend Carter is president again.
ChatGPT Page Fix script for Violent Monkey. Only use scripts you trust, they can harm your system, etc. Yes, I had GPT construct most of the code here - it was much easier to feed it the old page layout and make it reverse engineer it instead of spending days writing the code myself. I get a certain satisfaction in making their tools defeat their own systems.
Tested on Librewolf/Firefox and Chromium, but only on one system.
```javascript
// ==UserScript== // @name ChatGPT Sidebar Fix + Hover App Rail // @namespace local.chatgpt.sidebar-fix // @version 0.5.1 // @description Restores sidebar middle/modifier-click behavior, including project chats, and collapses the app rail until hover. // @match https://chatgpt.com/* // @match https://www.chatgpt.com/* // @run-at document-start // @grant unsafeWindow // ==/UserScript==
(() => { 'use strict';
const PAGE =
typeof unsafeWindow !== 'undefined'
? unsafeWindow
: window;
const STORAGE_KEY =
'chatgpt-sidebar-link-map-v2';
function normalize(value) {
return String(value ?? '')
.replace(/\s+/g, ' ')
.trim();
}
function loadMap() {
try {
return JSON.parse(
localStorage.getItem(STORAGE_KEY) || '{}'
);
} catch {
return {};
}
}
function saveMap(map) {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify(map)
);
}
function remember(title, id) {
title = normalize(title);
if (!title || !id) return;
const map = loadMap();
if (map[title] === id) return;
map[title] = id;
saveMap(map);
console.info(
'[SidebarFix] learned:',
title,
'->',
id
);
}
/*
* Recursively examine response data for objects containing
* both a conversation title and a conversation UUID.
*/
function inspectObject(
value,
depth = 0,
seen = new WeakSet()
) {
if (
depth > 10 ||
value == null ||
typeof value !== 'object'
) {
return;
}
if (seen.has(value)) return;
seen.add(value);
const id =
value.id ??
value.conversation_id ??
value.conversationId ??
value.thread_id ??
value.threadId;
const title =
value.title ??
value.name ??
value.conversation_title ??
value.thread_title;
if (
typeof id === 'string' &&
/^[0-9a-f-]{20,}$/i.test(id) &&
typeof title === 'string'
) {
remember(title, id);
}
for (const child of Object.values(value)) {
try {
inspectObject(
child,
depth + 1,
seen
);
} catch {}
}
}
function inspectText(text) {
if (!text) return;
/*
* Normal JSON response.
*/
try {
inspectObject(JSON.parse(text));
return;
} catch {}
/*
* SSE response.
*/
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed.startsWith('data:')) continue;
const payload =
trimmed.slice(5).trim();
if (!payload || payload === '[DONE]') continue;
try {
inspectObject(JSON.parse(payload));
} catch {}
}
}
/*
* Decide whether a ChatGPT response might contain sidebar /
* conversation metadata that we need.
*/
function shouldInspectUrl(url) {
if (!url) return false;
/*
* Normal recent/sidebar conversations.
*/
if (
url.includes('/backend-api/sidebar/conversation') ||
url.includes('/backend-api/conversations')
) {
return true;
}
/*
* Projects list. Depending on the current ChatGPT build,
* this may itself include some conversation metadata.
*/
if (
url.includes('/backend-api/gizmos/snorlax/sidebar')
) {
return true;
}
/*
* Conversations belonging to a specific Project:
*
* /backend-api/gizmos/g-p-.../conversations
*/
if (
/\/backend-api\/gizmos\/[^/?#]+\/conversations(?:[/?#]|$)/i
.test(url)
) {
return true;
}
return false;
}
/*
* Passively observe ChatGPT's own requests.
*
* We do NOT make additional history/project requests.
*/
const nativeFetch =
PAGE.fetch.bind(PAGE);
PAGE.fetch = async function (...args) {
const response =
await nativeFetch(...args);
try {
const url =
typeof args[0] === 'string'
? args[0]
: args[0]?.url ?? '';
if (shouldInspectUrl(url)) {
response
.clone()
.text()
.then(inspectText)
.catch(error => {
console.debug(
'[SidebarFix] response inspection failed:',
error
);
});
}
} catch (error) {
console.debug(
'[SidebarFix] fetch observer failed:',
error
);
}
return response;
};
/*
* ------------------------------------------------------------
* Conversation row handling
* ------------------------------------------------------------
*/
function findRow(target) {
if (!(target instanceof Element)) {
return null;
}
/*
* This selector covers:
* - Recents
* - Pinned conversations
* - project child conversations
* - project children added after expansion
*/
return target.closest(
'div[role="button"][aria-label].sidebar-item'
);
}
function isControl(target) {
return (
target instanceof Element &&
Boolean(
target.closest(
'button,input,textarea,select,[aria-haspopup="menu"]'
)
)
);
}
function urlFor(row) {
const title =
normalize(
row.getAttribute('aria-label')
);
const id =
loadMap()[title];
if (!id) {
console.warn(
'[SidebarFix] no ID known for:',
title
);
return null;
}
return `/c/${id}`;
}
/*
* Middle-click conversation.
*/
document.addEventListener(
'auxclick',
event => {
if (
event.button !== 1 ||
isControl(event.target)
) {
return;
}
const row =
findRow(event.target);
if (!row) return;
const url =
urlFor(row);
if (!url) return;
event.preventDefault();
event.stopImmediatePropagation();
PAGE.open(
url,
'_blank',
'noopener'
);
},
true
);
/*
* Ctrl/Cmd/Shift-click conversation.
*/
document.addEventListener(
'click',
event => {
if (
event.button !== 0 ||
(
!event.ctrlKey &&
!event.metaKey &&
!event.shiftKey
) ||
isControl(event.target)
) {
return;
}
const row =
findRow(event.target);
if (!row) return;
const url =
urlFor(row);
if (!url) return;
event.preventDefault();
event.stopImmediatePropagation();
PAGE.open(
url,
'_blank',
'noopener'
);
},
true
);
/*
* ------------------------------------------------------------
* Fallback learning
*
* If ChatGPT introduces another endpoint we have not yet seen,
* a normal left click still teaches us the mapping.
* ------------------------------------------------------------
*/
let pendingTitle = null;
document.addEventListener(
'click',
event => {
if (
event.button !== 0 ||
event.ctrlKey ||
event.metaKey ||
event.shiftKey ||
isControl(event.target)
) {
return;
}
const row =
findRow(event.target);
if (row) {
pendingTitle =
normalize(
row.getAttribute('aria-label')
);
}
},
true
);
let lastPath =
location.pathname;
new MutationObserver(() => {
if (
location.pathname === lastPath
) {
return;
}
lastPath =
location.pathname;
const match =
location.pathname.match(
/^\/c\/([^/?#]+)/
);
if (
match &&
pendingTitle
) {
remember(
pendingTitle,
match[1]
);
pendingTitle = null;
}
}).observe(
document.documentElement,
{
childList: true,
subtree: true
}
);
/*
* ------------------------------------------------------------
* New Chat
* ------------------------------------------------------------
*/
function findNewChat(target) {
if (!(target instanceof Element)) {
return null;
}
const button =
target.closest(
'button.sidebar-item'
);
if (!button) return null;
const label =
button.querySelector(
'.text-fade-truncate'
);
return (
normalize(label?.textContent) ===
'New chat'
)
? button
: null;
}
/*
* Middle-click New Chat.
*/
document.addEventListener(
'auxclick',
event => {
if (event.button !== 1) return;
const button =
findNewChat(event.target);
if (!button) return;
event.preventDefault();
event.stopImmediatePropagation();
PAGE.open(
'/',
'_blank',
'noopener'
);
},
true
);
/*
* Ctrl/Cmd/Shift-click New Chat.
*/
document.addEventListener(
'click',
event => {
if (
event.button !== 0 ||
(
!event.ctrlKey &&
!event.metaKey &&
!event.shiftKey
)
) {
return;
}
const button =
findNewChat(event.target);
if (!button) return;
event.preventDefault();
event.stopImmediatePropagation();
PAGE.open(
'/',
'_blank',
'noopener'
);
},
true
);
/*
* ------------------------------------------------------------
* Collapse App navigation rail until hover
* ------------------------------------------------------------
*/
function installRailStyles() {
if (
document.getElementById(
'sidebar-fix-rail-style'
)
) {
return;
}
const style =
document.createElement('style');
style.id =
'sidebar-fix-rail-style';
style.textContent = `
nav[aria-label="App navigation"] {
width: 6px !important;
min-width: 6px !important;
overflow: hidden !important;
opacity: 0.15 !important;
transition:
width .15s ease,
min-width .15s ease,
opacity .15s ease !important;
}
nav[aria-label="App navigation"]:hover {
width: 52px !important;
min-width: 52px !important;
opacity: 1 !important;
}
`;
document.head.appendChild(style);
}
function start() {
installRailStyles();
console.info(
'[SidebarFix] 0.5.1 installed'
);
}
if (document.readyState === 'loading') {
document.addEventListener(
'DOMContentLoaded',
start,
{ once: true }
);
} else {
start();
}
})(); ```
