DEV Community

vast cow
vast cow

Posted on • Edited on

Preventing `\n` from Entering Codex Prompts

When using Codex, unwanted line breaks can sometimes appear in the prompt field. This can make prompts harder to read, edit, and send in a clean format. This script is designed to prevent that problem in ChatGPT Codex Cloud.

What This Script Does

This UserScript watches the prompt input area in Codex Cloud and automatically removes newline characters that appear in the text.

More specifically, it monitors the #prompt-textarea element and cleans up line breaks found in its direct paragraph elements. As a result, prompts stay in a single-line format instead of being split across multiple lines unexpectedly.

The script also tries to preserve the caret position after cleaning the text. This helps keep typing smooth and reduces disruption while editing.

Why Use It

The main purpose of this script is to make prompt entry more stable and easier to manage.

It can be useful when you want to:

  • avoid accidental line breaks in prompts
  • keep prompts in a clean, single-line format
  • reduce editing issues caused by unexpected newlines

From a user perspective, it works as a simple helper that automatically removes unwanted line breaks while you type.

How to Use It

Add It as a UserScript

This code is meant to be used as a UserScript. In practice, that means installing a browser extension that supports UserScripts and then adding this script to it.

Once it is installed, the script runs automatically on the matching Codex Cloud pages.

Supported Pages

The script is set to run on these URLs:

  • https://chatgpt.com/codex/cloud
  • https://chatgpt.com/codex/cloud/*

When you open one of these pages, the script starts working automatically.

How It Works in Practice

After the page loads, the script waits for the prompt editor to appear. Once the editor is available, it starts listening for input changes.

Each time you type, the script checks whether newline characters have appeared in the prompt. If they have, it removes them and then restores the caret as naturally as possible.

Things to Keep in Mind

It Is Best for Single-Line Prompts

Because this script removes line breaks, it is best suited for workflows where prompts are meant to stay on one line. It is not a good fit if you intentionally want to write multi-line prompts.

It Helps Keep Input Clean

This script is especially useful for people who want a simpler, cleaner prompt field and do not want formatting problems caused by unexpected newlines.

Conclusion

This UserScript is a simple way to prevent unwanted newlines in Codex Cloud prompts. It watches the input field, removes line breaks automatically, and keeps the caret position as stable as possible.

You do not need to understand the internal implementation in detail to use it. In practical terms, it is a lightweight tool for keeping Codex prompts clean and easy to edit.

// ==UserScript==
// @name         ChatGPT Codex Cloud - Remove Newlines in Prompt
// @namespace    https://chatgpt.com/
// @version      1.1.2-debug
// @description  Remove CR/LF from direct <p> children on paste event with detailed debug logs.
// @match        https://chatgpt.com/codex/cloud
// @match        https://chatgpt.com/codex/cloud/*
// @grant        none
// ==/UserScript==

(() => {
  "use strict";

  const LOG_PREFIX = "[CodexPromptNLDebug]";
  const SCRIPT_VERSION = "1.1.2-debug";

  let observerA = null; // waits for #prompt-textarea to appear
  let observerB = null; // watches #prompt-textarea replacement/removal
  let currentEditor = null;
  let pasteHandler = null;

  let attachCount = 0;
  let pasteCount = 0;
  let sanitizeCount = 0;
  let observerACallbackCount = 0;
  let observerBCallbackCount = 0;

  function log(...args) {
    console.log(LOG_PREFIX, ...args);
  }

  function warn(...args) {
    console.warn(LOG_PREFIX, ...args);
  }

  function error(...args) {
    console.error(LOG_PREFIX, ...args);
  }

  function describeNode(node) {
    if (!node) return null;

    return {
      nodeName: node.nodeName,
      id: node.id || null,
      className: typeof node.className === "string" ? node.className : null,
      isConnected: node.isConnected,
      childElementCount: node.childElementCount,
      textLength: node.textContent?.length ?? null,
      htmlLength: node.innerHTML?.length ?? null,
    };
  }

  function getEditor() {
    return document.querySelector("#prompt-textarea");
  }

  function getParagraphs(editor = getEditor()) {
    if (!editor) return [];
    return editor.querySelectorAll(":scope > p");
  }

  function sanitizeParagraphs(reason = "unknown") {
    sanitizeCount += 1;

    log("sanitizeParagraphs:start", {
      sanitizeCount,
      reason,
      href: location.href,
      activeElement: describeNode(document.activeElement),
      currentEditor: describeNode(currentEditor),
      foundEditor: describeNode(getEditor()),
    });

    try {
      const editor = getEditor();

      if (!editor) {
        warn("sanitizeParagraphs:editor-not-found", {
          sanitizeCount,
          reason,
        });
        return;
      }

      const paragraphs = getParagraphs(editor);

      log("sanitizeParagraphs:editor-found", {
        sanitizeCount,
        reason,
        editor: describeNode(editor),
        paragraphCount: paragraphs.length,
      });

      let changedCount = 0;

      paragraphs.forEach((p, index) => {
        const before = p.innerHTML;
        const after = before.replaceAll("\r", "").replaceAll("\n", "");

        const hasCR = before.includes("\r");
        const hasLF = before.includes("\n");

        log("sanitizeParagraphs:paragraph-check", {
          sanitizeCount,
          index,
          hasCR,
          hasLF,
          beforeHtmlLength: before.length,
          afterHtmlLength: after.length,
          textLength: p.textContent?.length ?? null,
        });

        if (before !== after) {
          changedCount += 1;

          log("sanitizeParagraphs:paragraph-changed", {
            sanitizeCount,
            index,
            before,
            after,
          });

          p.innerHTML = after;
        } else {
          log("sanitizeParagraphs:paragraph-unchanged", {
            sanitizeCount,
            index,
          });
        }
      });

      log("sanitizeParagraphs:done", {
        sanitizeCount,
        reason,
        paragraphCount: paragraphs.length,
        changedCount,
      });
    } catch (err) {
      error("sanitizeParagraphs:error", {
        sanitizeCount,
        reason,
        errorName: err?.name,
        errorMessage: err?.message,
        stack: err?.stack,
      });
    }
  }

  function detachFromCurrentEditor(reason = "unknown") {
    log("detachFromCurrentEditor:start", {
      reason,
      currentEditor: describeNode(currentEditor),
      hasPasteHandler: Boolean(pasteHandler),
    });

    try {
      if (currentEditor && pasteHandler) {
        currentEditor.removeEventListener("paste", pasteHandler);
        log("detachFromCurrentEditor:paste-listener-removed", {
          reason,
        });
      } else {
        log("detachFromCurrentEditor:no-listener-to-remove", {
          reason,
        });
      }
    } catch (err) {
      error("detachFromCurrentEditor:error", {
        reason,
        errorName: err?.name,
        errorMessage: err?.message,
        stack: err?.stack,
      });
    } finally {
      currentEditor = null;
      pasteHandler = null;
    }
  }

  function attachToEditor(editor, reason = "unknown") {
    attachCount += 1;

    log("attachToEditor:start", {
      attachCount,
      reason,
      editor: describeNode(editor),
      sameAsCurrentEditor: editor === currentEditor,
      hasExistingPasteHandler: Boolean(pasteHandler),
      currentEditor: describeNode(currentEditor),
      hasObserverA: Boolean(observerA),
      hasObserverB: Boolean(observerB),
    });

    if (!editor) {
      warn("attachToEditor:called-with-empty-editor", {
        attachCount,
        reason,
      });
      return;
    }

    if (editor === currentEditor && pasteHandler && currentEditor?.isConnected) {
      log("attachToEditor:already-attached-same-connected-editor", {
        attachCount,
        reason,
      });
      return;
    }

    if (currentEditor && pasteHandler) {
      detachFromCurrentEditor("reattach-to-editor");
    }

    currentEditor = editor;

    pasteHandler = (e) => {
      pasteCount += 1;

      log("paste:event-fired", {
        pasteCount,
        eventType: e.type,
        target: describeNode(e.target),
        currentTarget: describeNode(e.currentTarget),
        clipboardTypes: Array.from(e.clipboardData?.types ?? []),
        href: location.href,
      });

      setTimeout(() => {
        log("paste:setTimeout-fired", {
          pasteCount,
          currentEditorConnected: currentEditor?.isConnected ?? null,
          currentEditor: describeNode(currentEditor),
          foundEditor: describeNode(getEditor()),
          paragraphCount: getParagraphs().length,
        });

        sanitizeParagraphs("paste-timeout-0");
      }, 0);
    };

    editor.addEventListener("paste", pasteHandler);

    log("attachToEditor:paste-listener-attached", {
      attachCount,
      reason,
    });

    if (observerA) {
      observerA.disconnect();
      observerA = null;

      log("attachToEditor:observerA-disconnected", {
        attachCount,
        reason,
      });
    }

    ensureObserverB();
  }

  function ensureObserverB() {
    if (observerB) {
      log("ensureObserverB:already-running");
      return;
    }

    observerB = new MutationObserver((mutations) => {
      observerBCallbackCount += 1;

      const foundEditor = getEditor();
      const currentConnected = currentEditor?.isConnected ?? false;
      const sameEditor = foundEditor === currentEditor;

      log("observerB:callback", {
        observerBCallbackCount,
        mutationCount: mutations.length,
        foundEditorExists: Boolean(foundEditor),
        currentConnected,
        sameEditor,
        currentEditor: describeNode(currentEditor),
        foundEditor: describeNode(foundEditor),
      });

      // Case 1:
      // #prompt-textarea が完全に消えた
      if (!foundEditor) {
        log("observerB:editor-not-found", {
          observerBCallbackCount,
        });

        detachFromCurrentEditor("editor-not-found");

        if (observerB) {
          observerB.disconnect();
          observerB = null;

          log("observerB:disconnected", {
            observerBCallbackCount,
          });
        }

        waitForEditor("editor-not-found");
        return;
      }

      // Case 2:
      // 新しい #prompt-textarea が存在するが、currentEditor は古い detached node を指している
      // 今回のログで発生していたのはこのケース
      if (!currentConnected || !sameEditor) {
        warn("observerB:editor-replaced-detected", {
          observerBCallbackCount,
          currentConnected,
          sameEditor,
          currentEditor: describeNode(currentEditor),
          foundEditor: describeNode(foundEditor),
        });

        attachToEditor(foundEditor, "observerB-detected-replacement");
        return;
      }
    });

    observerB.observe(document.documentElement, {
      childList: true,
      subtree: true,
    });

    log("ensureObserverB:started");
  }

  function waitForEditor(reason = "initial") {
    log("waitForEditor:start", {
      reason,
      href: location.href,
      readyState: document.readyState,
      hasObserverA: Boolean(observerA),
      hasObserverB: Boolean(observerB),
      currentEditor: describeNode(currentEditor),
    });

    const editor = getEditor();

    if (editor) {
      log("waitForEditor:editor-already-present", {
        reason,
        editor: describeNode(editor),
      });

      attachToEditor(editor, `waitForEditor:${reason}`);
      return;
    }

    if (observerA) {
      log("waitForEditor:observerA-already-running", {
        reason,
      });
      return;
    }

    observerA = new MutationObserver((mutations) => {
      observerACallbackCount += 1;

      const found = getEditor();

      log("observerA:callback", {
        observerACallbackCount,
        mutationCount: mutations.length,
        found: Boolean(found),
        foundEditor: describeNode(found),
      });

      if (found) {
        log("observerA:editor-appeared", {
          observerACallbackCount,
        });

        attachToEditor(found, "observerA-editor-appeared");
      }
    });

    observerA.observe(document.documentElement, {
      childList: true,
      subtree: true,
    });

    log("waitForEditor:observerA-started", {
      reason,
    });
  }

  log("userscript:initialized", {
    version: SCRIPT_VERSION,
    href: location.href,
    readyState: document.readyState,
    userAgent: navigator.userAgent,
  });

  waitForEditor("initial");
})();
Enter fullscreen mode Exit fullscreen mode

Top comments (0)