T3Chat Up-Arrow Edit

Press ↑ in an empty compose box to edit your last message

// ==UserScript==
// @name         T3Chat Up-Arrow Edit
// @version      0.1.1
// @description  Press ↑ in an empty compose box to edit your last message
// @match        https://t3.chat/*
// @match        https://*.t3.chat/*
// @run-at       document-idle
// @grant        none
// @namespace    wearifulpoet.com
// @license      MIT
// ==/UserScript==

(() => {
  'use strict';

  const INPUT_SELECTORS = [
    '#chat-input',
    'textarea[aria-describedby="chat-input-description"]',
    'textarea[placeholder*="message"]',
    'textarea[data-testid="chat-input"]',
  ];

  const MESSAGE_CONTAINER_SELECTOR = '[role="article"]';
  const MESSAGE_CONTENT_SELECTOR = '.prose';
  const EDIT_BUTTON_SELECTOR = 'button[aria-label="Edit message"]';

  const findChatInput = () =>
    INPUT_SELECTORS.map((s) => document.querySelector(s)).find(
      (el) => el && el.tagName === 'TEXTAREA',
    ) || null;

  const findLastUserMessage = () => {
    const containers = [...document.querySelectorAll(MESSAGE_CONTAINER_SELECTOR)];
    for (let i = containers.length - 1; i >= 0; i--) {
      const c = containers[i];
      const btn = c.querySelector(EDIT_BUTTON_SELECTOR);
      const txt = c.querySelector(MESSAGE_CONTENT_SELECTOR);
      if (btn && txt?.textContent.trim()) return { container: c, editButton: btn };
    }
    return null;
  };

  const scrollToMessage = (el) => {
    if (!el) return;
    try {
      el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
    } catch {
      el.scrollIntoView(true);
    }
  };

  const handleUpArrow = (e) => {
    if (e.key !== 'ArrowUp' || e.isComposing) return;
    const input = e.currentTarget;
    if (input.value.trim() || input.selectionStart !== 0 || input.selectionEnd !== 0) return;

    const last = findLastUserMessage();
    if (!last) return;

    e.preventDefault();
    last.editButton.click();
    setTimeout(() => scrollToMessage(last.container), 150);
  };

  const attach = (input) => {
    if (input.dataset.arrowEditBound === '1') return;
    input.addEventListener('keydown', handleUpArrow);
    input.dataset.arrowEditBound = '1';
  };

  const bind = () => {
    const input = findChatInput();
    if (input) attach(input);
  };

  const observer = new MutationObserver((muts) => {
    for (const mut of muts) {
      for (const node of mut.addedNodes) {
        if (node.nodeType !== 1) continue;
        if (
          INPUT_SELECTORS.some(
            (s) => node.matches?.(s) || node.querySelector?.(s),
          )
        ) {
          setTimeout(bind, 100);
          return;
        }
      }
    }
  });

  const init = () => {
    bind();
    observer.observe(document.documentElement, { childList: true, subtree: true });
  };

  if (document.readyState === 'loading')
    document.addEventListener('DOMContentLoaded', init);
  else init();
})();