Hi Inspired Ogre! I wrote this scriptlet using chatGPT in about ten minutes, so I make no guarantees. It looks better in dark mode, where the CRT "window" is not surrounded by handout white. Any player can type !crt something cool here. This will create a live updating handout called "CRT". Every time you enter a line, the handout will update. It will use whatever the typer's "Speaking As" is set to at the bottom of chat. So the GM could types as any character in the game. To clear the handout, type: !crt --clear Only the GM can clear the handout. Since these are API calls, they will not appear in chat and will only appear in the handout. If you want to create a new, fresh conversation, but preserve the old one, just make a copy of the existing CRT handout. Here is an animated demo. And here is the code: on('ready', () => { const HANDOUT_NAME = "CRT"; const sanitize = (str) => { return (str || "").replace(/[<>&"]/g, c => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' })[c]); }; const CRT_WRAPPER_START = `<div id="crt" style="background:black;color:#33ff33;font-family:monospace;font-size:14px;padding:10px;">`; const CRT_WRAPPER_END = `</div>`; const baseCRTContent = `${CRT_WRAPPER_START} ${CRT_WRAPPER_END}`; const getOrCreateHandout = () => { let ho = findObjs({ type: 'handout', name: HANDOUT_NAME })[0]; if (!ho) { ho = createObj("handout", { name: HANDOUT_NAME }); ho.set("notes", baseCRTContent); } return ho; }; const clearHandout = () => { const ho = getOrCreateHandout(); ho.set("notes", baseCRTContent); }; const appendToHandout = (speaker, text) => { const ho = getOrCreateHandout(); ho.get("notes", notes => { let content = notes || ""; const startIndex = content.indexOf(CRT_WRAPPER_START); const endIndex = content.lastIndexOf(CRT_WRAPPER_END); if (startIndex === -1 || endIndex === -1) { content = baseCRTContent; } const before = content.slice(0, content.lastIndexOf(CRT_WRAPPER_END)); const after = CRT_WRAPPER_END; const newLine = `${speaker}: ${text}<br>\n`; const updated = before + newLine + after; ho.set("notes", updated); }); }; on('chat:message', msg => { if (msg.type !== 'api') return; if (!msg.content.startsWith('!crt')) return; const parts = msg.content.trim().split(/\s+/); // CLEAR COMMAND if (parts[1] === "--clear") { if (!playerIsGM(msg.playerid)) { sendChat("CRT", "/w " + msg.who + " Only the GM may clear the CRT."); return; } clearHandout(); sendChat("CRT", "/w gm CRT handout cleared."); return; } // NORMAL APPEND MODE const args = msg.content.replace(/^!crt\s*/, ""); if (!args.trim()) return; const text = sanitize(args.trim()); let speaker = msg.who ? msg.who.replace(/\s*\(GM\)\s*$/, "") : "Unknown"; if (msg.characterid) { const char = getObj("character", msg.characterid); if (char) { speaker = char.get("name"); } } appendToHandout(speaker, text); }); });