Skip to content

Instantly share code, notes, and snippets.

@wilmoore
Last active January 8, 2026 14:39
Show Gist options
  • Select an option

  • Save wilmoore/4b1ab52358668394ba096bf6016d74e6 to your computer and use it in GitHub Desktop.

Select an option

Save wilmoore/4b1ab52358668394ba096bf6016d74e6 to your computer and use it in GitHub Desktop.
Software Engineering :: Web :: Browser :: Extension :: Development :: Conversation Title for ChatGPT

Software Engineering :: Web :: Browser :: Extension :: Development :: Conversation Title for ChatGPT

⪼ Made with 💜 by Polyglot.

inspiration
related
reference
sources
store

Conversation Title for ChatGPT

Figma

Manage

  • chrome://extensions

Source

ChatGPT

Bottom Disclaimer Div

document.querySelector('div.min-h-4 > div').textContent = document.title

Find & Set The Empty Title Div

document.querySelector('main [aria-haspopup="menu"]').parentElement.previousSibling.textContent = "Crixus: Rebel Gladiator Leader";

Find the <title>ChatGPT</title>

document.querySelector('title').textContent.trim()

Find the <h3 ...>Today

document.querySelector('h3[class~=text-token-text-tertiary]')

Get the first li

h3.parentElement.parentElement.querySelector('li a div').textContent

Selector One-Liner

document.querySelector('h3[class~=text-token-text-tertiary]').parentElement.parentElement.querySelector('li a div').textContent.trim()

Logic

  1. When the conversation is new, the <Title /> tag's content will contain the text "ChatGPT".
  2. When the conversation is new, the title should be extracted from the sidebar.
  3. Otherwise, it should come from the <Title /> tag.
Extension Source

effectiveTitle

const effectiveTitle = () => {
  const documentTitle = document.querySelector('title').textContent.trim();
  const sidebarTitle = document.querySelector('nav[aria-label="Chat history"] ol li:first-child a div').textContent.trim()

  return documentTitle === "ChatGPT" ? sidebarTitle : documentTitle;
}

document.querySelector('main [aria-haspopup="menu"]').parentElement.previousSibling.textContent = effectiveTitle();

HTML

<div class="flex items-center text-lg font-medium">Crixus: Rebel Gladiator Leader</div>

YouTube

Click To Copy One-Liner
document.querySelector('#title > h1 > yt-formatted-string').addEventListener('click', (event) => navigator.clipboard.writeText(event.target.textContent).then(alert('title copied')).catch('unable to copy title'));
Click To Highlight One-Liner
document.querySelector('#title > h1 > yt-formatted-string').addEventListener('click', e => { const r = document.createRange(), s = window.getSelection(); s.removeAllRanges(); r.selectNodeContents(e.target); s.addRange(r); });

Mutation Observer Code

// Function to be called when the text content of either selector changes
function handleMutation(mutationsList, observer) {
    const updateTitle = () => {
      const documentTitle = document.querySelector('title').textContent.trim();
      const sidebarTitle = document.querySelector('nav[aria-label="Chat history"] ol li:first-child a div').textContent.trim();

      document.querySelector('main [aria-haspopup="menu"]').parentElement.previousSibling.textContent = "ChatGPT" ? sidebarTitle : documentTitle;
    }

    for (const mutation of mutationsList) {
        if (mutation.type === 'characterData' || mutation.type === 'childList') {
            console.log(`Mutation detected: ${mutation.target.textContent}`);
            updateTitle();
        }
    }
}

// Create a MutationObserver instance
const observer = new MutationObserver(handleMutation);

// Configuration options for the observer (observe changes to text content and child nodes)
const config = { characterData: true, subtree: true, childList: true };

// Function to observe a target element
function observeTarget(selector) {
    const target = document.querySelector(selector);
    if (target) {
        observer.observe(target, config);
    } else {
        console.warn(`No element found for selector: ${selector}`);
    }
}

// Selectors to observe
const selectors = [
    'title',
    'nav[aria-label="Chat history"] ol li:first-child a div'
];

// Observe each selector
selectors.forEach(observeTarget);

Debounced Mutation Observer

const updateTitle = () => {
  let effectiveTitle = "";
  const documentTitle = document.querySelector('title').textContent.trim();
  const sidebarTitle = document.querySelector('nav[aria-label="Chat history"] ol li:first-child a div').textContent.trim();
  
  if (documentTitle === "ChatGPT") {
    effectiveTitle = "(Untitled)";
  } else {
    effectiveTitle = documentTitle;
  }

  console.log("Title Updated", effectiveTitle);
  document.querySelector('main [aria-haspopup="menu"]').parentElement.previousSibling.textContent = effectiveTitle;
}

// Debounce function to delay the callback execution
function debounce(callback, delay) {
    let timeoutId;
    return (...args) => {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
            callback(...args);
        }, delay);
    };
}

// Function to be called when the text content of either selector changes
function handleMutation(mutationsList, observer) {
    for (const mutation of mutationsList) {
        if (mutation.type === 'characterData' || mutation.type === 'childList') {
            console.log(`Mutation detected: ${mutation.target.textContent}`);
            updateTitle();
        }
    }
}

// Debounced version of the handleMutation function
const debouncedHandleMutation = debounce(handleMutation, 300); // 300ms delay

// Create a MutationObserver instance with the debounced callback
const observer = new MutationObserver(debouncedHandleMutation);

// Configuration options for the observer (observe changes to text content and child nodes)
const config = { characterData: true, subtree: true, childList: true };

// Function to observe a target element
function observeTarget(selector) {
    const target = document.querySelector(selector);
    if (target) {
        observer.observe(target, config);
    } else {
        console.warn(`No element found for selector: ${selector}`);
    }
}

// Selectors to observe
const selectors = [
    'title',
    'nav[aria-label="Chat history"] ol li:first-child a div'
];

// Observe each selector
selectors.forEach(observeTarget);

Functions

const getSelectedSidebarItem = () => {
  const SELECTOR = 'nav[aria-label="Chat history"] ol li a div[class~="from-token-sidebar-surface-secondary"]';
  
  return document.querySelector(SELECTOR)
    ? document.querySelector(SELECTOR).parentElement
    : null;
}
const isSidebarItemSelected = () => !!getSelectedSidebarItem();

const getDocumentTitle = () => document.querySelector('title').textContent.trim();
const getSidebarTitle = () => isSidebarItemSelected() ? getSelectedSidebarItem().textContent : "";

const hasConversationId = () => /(?<ConversationId>[a-fA-F0-9]{32})/.test(window.location.pathname.replace(/[-]/g, ""));
const conversationIdIsEmpty = () => !hasConversationId();
const documentTitleIsEmpty = () => getDocumentTitle() === "ChatGPT";
const hasDocumentTitle = () => !documentTitleIsEmpty();

const getTitleElement = () => document.querySelector('main [aria-haspopup="menu"]').parentElement.previousSibling;

const injectConversationTitle = () => {
  if (conversationIdIsEmpty() && documentTitleIsEmpty()) {
    getTitleElement().textContent = "Untitled Conversation (ChatGPT)";
  }
    
  if (hasConversationId() && hasDocumentTitle()) {
    getTitleElement().textContent = getDocumentTitle();
  }

  if (hasConversationId() && isSidebarItemSelected()) {
    getTitleElement().textContent = getSidebarTitle();
  }
}

function executeWithExponentialBackoff(fn, minDelay, maxDelay, attempts = 1) {
  const delay = Math.min(maxDelay, minDelay * Math.pow(2, attempts - 1));

  setTimeout(() => {
    fn();
    if (delay < maxDelay) {
      executeWithExponentialBackoff(fn, minDelay, maxDelay, attempts + 1);
    } else {
      executeWithExponentialBackoff(fn, minDelay, maxDelay);
    }
  }, delay * 1000);
}

injectConversationTitle();
// Start the execution with exponential backoff
setInterval(injectConversationTitle, 3000);

Mutation Observer

const getSelectedSidebarItem = () => {
  const SELECTOR = 'nav[aria-label="Chat history"] ol li a div[class~="from-token-sidebar-surface-secondary"]';
  const selectedElementAnchor = document.querySelector(SELECTOR);
  const selectedElement = selectedElementAnchor && selectedElementAnchor.parentElement;

  return selectedElement
    ? selectedElement
    : null;
}

const isSidebarItemSelected = () => !!getSelectedSidebarItem();

const getDocumentTitle = () => document.querySelector('title').textContent.trim();
const getSidebarTitle = () => isSidebarItemSelected() ? getSelectedSidebarItem().textContent : "";

const hasConversationId = () => /(?<ConversationId>[a-fA-F0-9]{32})/.test(window.location.pathname.replace(/[-]/g, ""));
const conversationIdIsEmpty = () => !hasConversationId();
const documentTitleIsEmpty = () => getDocumentTitle() === "ChatGPT";
const hasDocumentTitle = () => !documentTitleIsEmpty();

const getTitleElement = () => document.querySelector('main [aria-haspopup="menu"]').parentElement.previousSibling;

const injectConversationTitle = () => {
  if (conversationIdIsEmpty() && documentTitleIsEmpty() && getTitleElement()) {
    getTitleElement().textContent = "Untitled Conversation (ChatGPT)";
  }
    
  if (hasConversationId() && hasDocumentTitle() && getTitleElement()) {
    getTitleElement().textContent = getDocumentTitle();
  }

  if (hasConversationId() && isSidebarItemSelected() && getTitleElement()) {
    getTitleElement().textContent = getSidebarTitle();
  }
  
  getTitleElement() && getTitleElement().addEventListener('click', (event) => {
    const range = document.createRange();
    const selection = window.getSelection();
    selection.removeAllRanges();
    range.selectNodeContents(event.target);
    selection.addRange(range);
  });
}

const monitorSidebarForChanges = () => {
  // Select the node that will be observed for mutations
  const targetNode = document.querySelector('nav[aria-label="Chat history"] ol');

  // Options for the observer (which mutations to observe)
  const config = { characterData: true, subtree: true, childList: true, attributes: true };

  // Callback function to execute when mutations are observed
  const callback = (mutationList, observer) => {
    for (const mutation of mutationList) setTimeout(injectConversationTitle, 600);
  };

  // Create an observer instance linked to the callback function
  const observer = new MutationObserver(callback);

  // Start observing the target node for configured mutations
  observer.observe(targetNode, config);
}

monitorSidebarForChanges();
injectConversationTitle();
setInterval(injectConversationTitle, 2000);

Title-Based Mutation Observer

(function() {
  // Function to log the current document title
  function logTitleChange() {
    console.log('Title changed:', document.title);
  }

  // MutationObserver to detect changes in the document title
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.type === 'childList') {
        logTitleChange();
      }
    });
  });

  // Configuration of the observer
  const config = { childList: true };

  // Observe changes to the document title element
  const titleElement = document.querySelector('title');
  if (titleElement) {
    observer.observe(titleElement, config);
  } else {
    console.warn('No title element found');
  }

  // Initial log of the current document title
  logTitleChange();
})();

...With Idempotency

(function() {
  let idempotency = '';
  
  // Function to log the current document title
  function logTitleChange() {
    const conversationId = window.location.pathname.split(/[/]/).pop()
    if (conversationId === idempotency) return
    idempotency = conversationId
    console.log('Title changed:', conversationId);
  }

  // MutationObserver to detect changes in the document title
  const observer = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
      if (mutation.type === 'childList') {
        logTitleChange();
      }
    });
  });

  // Configuration of the observer
  const config = { childList: true };

  // Observe changes to the document title element
  const titleElement = document.querySelector('title');
  if (titleElement) {
    observer.observe(titleElement, config);
  } else {
    console.warn('No title element found');
  }

  // Initial log of the current document title
  logTitleChange();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment