Skip to content

Instantly share code, notes, and snippets.

@mohitmun
Last active October 11, 2025 07:36
Show Gist options
  • Select an option

  • Save mohitmun/3063340a7eb7058b24651dcc9a967a49 to your computer and use it in GitHub Desktop.

Select an option

Save mohitmun/3063340a7eb7058b24651dcc9a967a49 to your computer and use it in GitHub Desktop.
gpt_starred_feature.js - Use it your own risk. ChatGPT account might get blocked
// ==UserScript==
// @name ChatGPT – Pin Starred Chats + Visible White Star Toggle
// @namespace mohit.chatgpt.star.pin.fixed
// @version 1.0.1
// @description Pins starred chats to top, shows visible white-outline star icons, and adds a toggle beside Share. Uses Authorization Bearer + cookies.
// @match https://chat.openai.com/*
// @match https://chatgpt.com/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
"use strict";
const STAR_QUERY = {
offset: 0,
limit: 50,
order: "updated",
is_archived: "false",
is_starred: "true",
};
const CLASSES = {
rowStarIcon: "cgpt-row-star-icon",
headerStarBtn: "cgpt-header-star-btn",
processedRow: "cgpt-row-processed",
};
const SELECTORS = {
nav: [
'nav[aria-label="Chat history"]',
'aside nav[aria-label]',
'div[role="navigation"] nav',
'aside:has(nav) nav'
],
chatRow: [
'nav[aria-label="Chat history"] a[href^="/c/"][role="link"]',
'nav[aria-label="Chat history"] a[href^="/c/"]'
],
shareBtn: [
'button[aria-label*="Share"]',
'[data-testid="share-conversation-button"]',
'button:has(svg[aria-label*="Share"])'
],
convHeader: [
'[data-testid="conversation-header"]',
'header:has(button)',
'div:has(> h1):has(button)'
],
};
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const log = (...a) => console.log("[StarPin]", ...a);
function qsAny(root, arr) { for (const s of arr) { const el = (root || document).querySelector(s); if (el) return el; } return null; }
function getConversationIdFromUrl() { const m = location.pathname.match(/\/c\/([a-z0-9-]+)/i); return m ? m[1] : null; }
async function getAccessToken() {
const r = await fetch("/api/auth/session", { credentials: "include" });
if (!r.ok) throw new Error("session fetch failed");
const j = await r.json();
if (!j?.accessToken) throw new Error("no accessToken");
return j.accessToken;
}
async function apiGetStarred() {
const token = await getAccessToken();
const usp = new URLSearchParams();
Object.entries(STAR_QUERY).forEach(([k, v]) => usp.set(k, String(v)));
const r = await fetch(`/backend-api/conversations?${usp}`, {
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
credentials: "include"
});
if (!r.ok) throw new Error(`GET starred -> ${r.status}`);
return r.json();
}
async function apiGetConversation(id) {
const token = await getAccessToken();
const r = await fetch(`/backend-api/conversation/${id}`, {
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
credentials: "include"
});
if (!r.ok) throw new Error(`GET conv -> ${r.status}`);
return r.json();
}
async function apiPatchConversation(id, patch) {
const token = await getAccessToken();
const r = await fetch(`/backend-api/conversation/${id}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
Accept: "application/json"
},
credentials: "include",
body: JSON.stringify(patch)
});
if (!r.ok) throw new Error(`PATCH conv -> ${r.status}`);
return r.json().catch(() => ({}));
}
/* ---------- STAR ICON ---------- */
function makeStarSVG({ filled = false, size = 16 } = {}) {
const svgNS = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("width", size);
svg.setAttribute("height", size);
svg.setAttribute("stroke", "#fff"); // ✅ force visible white
svg.setAttribute("stroke-width", "2");
svg.setAttribute("stroke-linecap", "round");
svg.setAttribute("stroke-linejoin", "round");
svg.style.opacity = "0.8";
const poly = document.createElementNS(svgNS, "polygon");
poly.setAttribute("points", "12 2 15 9 22 9 17 14 19 21 12 17 5 21 7 14 2 9 9 9");
if (filled) svg.setAttribute("fill", "#fff"); else svg.setAttribute("fill", "none");
svg.appendChild(poly);
return svg;
}
/* ---------- SIDEBAR ---------- */
function getSidebarNav() { return qsAny(document, SELECTORS.nav); }
function getAllChatRows() {
const nav = getSidebarNav(); if (!nav) return [];
return Array.from(nav.querySelectorAll(SELECTORS.chatRow.join(",")));
}
function getRowConversationId(aEl) {
const m = aEl.getAttribute("href")?.match(/\/c\/([a-z0-9-]+)/i);
return m ? m[1] : null;
}
function ensureRowStarIcon(aEl) {
if (aEl.querySelector("." + CLASSES.rowStarIcon)) return;
const iconWrap = document.createElement("span");
iconWrap.className = CLASSES.rowStarIcon;
iconWrap.style.display = "inline-flex";
iconWrap.style.alignItems = "center";
iconWrap.style.marginRight = "6px";
iconWrap.appendChild(makeStarSVG({ filled: false, size: 13 }));
aEl.prepend(iconWrap);
}
function removeRowStarIcon(aEl) {
const el = aEl.querySelector("." + CLASSES.rowStarIcon);
if (el) el.remove();
}
async function pinStarredToTop() {
const nav = getSidebarNav();
if (!nav) return;
let starredIds = new Set();
try {
const data = await apiGetStarred();
const items = data.items || data.data || data.conversations || [];
starredIds = new Set(items.map(i => i.id));
} catch (e) { log("load starred failed", e); }
const rows = getAllChatRows();
if (!rows.length) return;
const container = rows[0].parentElement;
const starred = [], others = [];
for (const row of rows) {
const id = getRowConversationId(row);
if (id && starredIds.has(id)) { ensureRowStarIcon(row); starred.push(row); }
else { removeRowStarIcon(row); others.push(row); }
}
const frag = document.createDocumentFragment();
starred.forEach(r => frag.appendChild(r));
others.forEach(r => frag.appendChild(r));
container.appendChild(frag);
}
/* ---------- HEADER STAR BUTTON ---------- */
let headerStarBtn = null;
function ensureHeaderStarToggle() {
if (headerStarBtn && document.body.contains(headerStarBtn)) return headerStarBtn;
const share = qsAny(document, SELECTORS.shareBtn);
const header = share?.closest("div") || qsAny(document, SELECTORS.convHeader);
if (!header) return null;
const btn = document.createElement("button");
btn.type = "button";
btn.className = CLASSES.headerStarBtn;
btn.title = "Star / Unstar conversation";
Object.assign(btn.style, {
marginLeft: "8px",
border: "none",
background: "transparent",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
padding: "2px",
color: "inherit"
});
btn.appendChild(makeStarSVG({ filled: false, size: 16 }));
btn.addEventListener("click", onHeaderStarClicked);
header.appendChild(btn);
headerStarBtn = btn;
return btn;
}
async function onHeaderStarClicked() {
const id = getConversationIdFromUrl();
if (!id) return;
try {
setHeaderBusy(true);
const conv = await apiGetConversation(id);
const desired = !conv?.is_starred;
await apiPatchConversation(id, { is_starred: desired });
await syncHeaderStarVisual();
await pinStarredToTop();
} catch (e) { console.error(e); alert("Star toggle failed: " + e.message); }
finally { setHeaderBusy(false); }
}
function setHeaderBusy(busy) {
if (!headerStarBtn) return;
headerStarBtn.style.opacity = busy ? "0.5" : "1";
headerStarBtn.style.pointerEvents = busy ? "none" : "auto";
}
async function syncHeaderStarVisual() {
const id = getConversationIdFromUrl();
if (!id) return;
const btn = ensureHeaderStarToggle();
if (!btn) return;
try {
const conv = await apiGetConversation(id);
const starred = !!conv?.is_starred;
btn.innerHTML = "";
btn.appendChild(makeStarSVG({ filled: starred, size: 16 }));
btn.title = starred ? "Unstar conversation" : "Star conversation";
} catch {
btn.innerHTML = "";
btn.appendChild(makeStarSVG({ filled: false, size: 16 }));
}
}
/* ---------- INIT ---------- */
async function bootstrap() {
for (let i = 0; i < 30; i++) {
if (getSidebarNav()) break;
await sleep(150);
}
await pinStarredToTop();
await syncHeaderStarVisual();
const mo = new MutationObserver(async () => {
ensureHeaderStarToggle();
await pinStarredToTop();
});
mo.observe(document.documentElement, { childList: true, subtree: true });
let last = location.pathname;
setInterval(async () => {
if (location.pathname !== last) {
last = location.pathname;
await syncHeaderStarVisual();
await pinStarredToTop();
}
}, 900);
}
if (document.readyState === "complete" || document.readyState === "interactive") bootstrap();
else document.addEventListener("DOMContentLoaded", bootstrap);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment