import fs from 'node:fs'; // ---- The message queue (js/message-queue.js) ------------------------------ // While a turn runs, the composer stays usable and what the user sends from it // waits in a queue, drawn at the bottom of the chat as the user's own messages // with Edit, Remove and Send now. A clean turn end sends the next queued // message as the next turn, one per turn. Stop overrides the running turn: the // next queued message goes in its place, and Send now does the same for its // own message, the rest staying queued. A turn that ends on its own without // finishing (error, retries or credits running out) sends nothing and puts // every queued message back in the composer, ahead of what the user is // typing, with its files and picked element. This evaluates the // REAL module against a small jQuery stub and drives those paths, then // text-asserts where app.js, ui.js, helpers.js and clarify.js hook into it. let failures = 0; function check(name, cond, detail) { if (cond) console.log('ok - ' + name); else { console.error('FAIL - ' + name + (detail ? ' [' + detail + ']' : '')); failures++; } } const read = (p) => fs.readFileSync(new URL(p, import.meta.url), 'utf8'); const MQ = read('../src/js/message-queue.js'); const APP = read('../src/js/app.js'); const UI = read('../src/js/ui.js'); const HELP = read('../src/js/helpers.js'); const CLARIFY = read('../src/tools/chat_ui/clarify.js'); const CSS = read('../src/css/styles.css'); const INDEX = read('../src/index.html'); const VITE = read('../vite.config.js'); const PKG = JSON.parse(read('../package.json')); function htmlEscape(text) { return String(text).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>') .replaceAll('"', '"').replaceAll("'", '''); } function nl_to_p(text) { return text.split(/\n\n+/).map(p => `

${p.replace(/\n/g, '
')}

`).join('') .replace(/

<\/p>/g, '').replace(/^

<\/p>|

<\/p>$/g, ''); } const tick = () => new Promise(r => setTimeout(r, 0)); function makeEnv({ flag = true, clickToEdit = true } = {}) { const s = { composer: '', drafts: 0, sends: [], toasts: [], announces: [], revoked: [], stops: 0, listExists: false, listHTML: '', // The Send/Stop button, as updateSendButtonState draws it mid-turn. send: { label: 'Stop', html: 'STOP', classes: new Set() }, quietTargets: [], clearedTargets: 0, copied: null, observed: 0, }; const handlers = {}; const list = { get length() { return s.listExists ? 1 : 0; }, 0: { contains: () => false }, remove() { s.listExists = false; s.listHTML = ''; return this; }, html(h) { s.listHTML = h; return this; }, find() { return { eq: () => ({ find: () => ({ length: 0, first() { return this; } }) }), length: 0, index: () => -1 }; }, }; const none = { length: 0, remove() { return this; }, toggleClass() { return this; } }; const box = { scrollTop: 0, scrollHeight: 500 }; const env = { s, handlers, isProcessing: false, document: { activeElement: null }, MutationObserver: class { observe() { s.observed++; } disconnect() {} }, console: { warn() {} }, setTimeout, htmlEscape, nl_to_p, send_svg: 'ARROW', stop_svg: 'STOP', editTargetLabel: (d) => '' + htmlEscape('<' + d.tag + '>') + '', chatBoxNearBottom: () => false, revokeAttachmentURL: (a) => s.revoked.push(a.name), applyDraftToComposer: (t) => { s.composer = t; }, saveComposerDraft: () => { s.drafts++; }, // A send that never claims its item models one called off during setup. // Tests resolve it by hand. sendChatMessage: (input, skip, opts) => new Promise((resolve) => { s.sends.push({ input, opts, resolve }); }), window: { FEATURE_FLAGS: { messageQueue: flag, clickToEdit }, attachedImages: [], fileCardHTML: (n) => `[file ${n}]`, _pendingEditTarget: null, _activeClarification: null, shouldAutoScroll: false, isMobileViewport: () => true, announce: (t) => s.announces.push(t), showToast: (t) => s.toasts.push(t), stopActiveTurn() { s.stops++; env.isProcessing = false; }, clearEditTarget() { env.window._pendingEditTarget = null; s.clearedTargets++; }, setEditTarget(t, opts) { env.window._pendingEditTarget = t; if (opts && opts.quiet) s.quietTargets.push(t); }, takeAttachments() { const t = env.window.attachedImages; env.window.attachedImages = []; return t; }, returnAttachments(l) { env.window.attachedImages = l.concat(env.window.attachedImages); }, }, }; env.$ = (arg) => { if (arg === env.document) return { on(evt, sel, fn) { handlers[evt + ' ' + sel] = fn; return this; } }; if (arg && arg.__rowId != null) { return { closest: () => ({ attr: () => String(arg.__rowId) }), attr: () => arg.__action }; } if (arg === '.chat-input-message') return { val: () => s.composer, focus() {}, 0: null }; if (arg === '.chat-box') { return { length: 1, 0: box, children: (sel) => (sel === '.message-queue' && s.listExists ? list : none), append() { s.listExists = true; return this; }, }; } if (arg === '.chat-box .message-queue') return s.listExists ? list : none; if (arg.startsWith && arg.startsWith('

s.send.classes.has(c), toggleClass(c, on) { if (on) s.send.classes.add(c); else s.send.classes.delete(c); return this; }, attr(k) { if (typeof k === 'string') return s.send.label; s.send.label = k['aria-label']; return this; }, html(h) { s.send.html = h; return this; }, }; } throw new Error('unexpected selector ' + arg); }; new Function('env', 'with (env) {\n' + MQ + '\n}')(env); const Q = env.window.MessageQueue; const act = (action, id, el = {}) => handlers['click .queued-message [data-action]'].call(Object.assign(el, { __rowId: id, __action: action })); const queueText = (text) => { s.composer = text; return Q.enqueueFromComposer(); }; return { env, s, Q, act, box, queueText }; } // --- Off, idle, and the clarifying-questions card -------------------------- { const { env, s, Q } = makeEnv({ flag: false }); env.isProcessing = true; s.composer = 'hello'; check('flag off: a send never joins the queue', !Q.takesComposerSend() && Q.enqueueFromComposer() === false && Q._items().length === 0); check('flag off: Stop never sends a queued message', Q.sendNext() === false && s.sends.length === 0); Q.syncButton(); check('flag off: the button stays Stop', s.send.label === 'Stop' && !s.send.classes.has('queueing')); } { const { env, s, Q } = makeEnv(); s.composer = 'hello'; check('no turn running: a send is a normal send', !Q.takesComposerSend() && Q.enqueueFromComposer() === false); env.isProcessing = true; env.window._activeClarification = {}; check('a clarifying-questions card takes the send as its answer', !Q.takesComposerSend()); Q.syncButton(); check('…and the button is left as the card draws it', s.send.label === 'Stop' && !s.send.classes.has('queueing')); env.window._activeClarification = null; s.composer = ' '; check('an empty composer queues nothing', Q.enqueueFromComposer() === false && Q._items().length === 0); } // --- Queueing --------------------------------------------------------------- { const { env, s, Q, box } = makeEnv(); env.isProcessing = true; s.composer = ''; Q.syncButton(); check('mid-turn with an empty composer: the button is Stop', s.send.label === 'Stop' && s.send.html === 'STOP'); s.composer = ' make the header blue\n\nand bold '; Q.syncButton(); check('mid-turn with text: Stop turns into the queue arrow (one button, not two)', s.send.label === 'Queue message' && s.send.html === 'ARROW' && s.send.classes.has('queueing')); const drafts = s.drafts; check('Enter mid-turn queues', Q.takesComposerSend() && Q.enqueueFromComposer() === true); const [a] = Q._items(); check('the queued text is trimmed', a && a.text === 'make the header blue\n\nand bold'); check('the composer is emptied', s.composer === ''); check('the draft is saved (it now holds the queued text)', s.drafts === drafts + 1); check('it shows in the chat as a message', s.listExists && s.listHTML.includes('queued-message-bubble')); check('…with its paragraphs, escaped', s.listHTML.includes('

make the header <b>blue</b>

and bold

')); check('…marked Queued', s.listHTML.includes('Queued')); check('…with Edit, Remove and Send now, and no Copy or shortcut hint', ['edit', 'remove', 'send-now'].every(x => s.listHTML.includes(`data-action="${x}"`)) && !s.listHTML.includes('data-action="copy"') && !s.listHTML.includes('')); check('the chat scrolls to it', box.scrollTop === box.scrollHeight && env.window.shouldAutoScroll === true); check('it is kept below what the turn adds', s.observed === 1); check('screen readers hear it was queued', s.announces.some(t => /queued/i.test(t))); check('…and is Stop again with the box empty', s.send.label === 'Stop' && s.send.html === 'STOP' && !s.send.classes.has('queueing')); env.window.attachedImages = [{ name: 'shot.png', type: 'image/png', blobURL: 'blob:x/1' }, { name: 'notes.pdf', type: 'application/pdf' }]; env.window._pendingEditTarget = { tag: 'button' }; s.composer = 'also this'; Q.enqueueFromComposer(); const b = Q._items()[1]; check('a queued message takes the tray files', b.attachments.length === 2 && env.window.attachedImages.length === 0); check('…and the picked element', b.editTarget && b.editTarget.tag === 'button' && env.window._pendingEditTarget === null && s.clearedTargets === 1); check('…and shows them like a sent message does', s.listHTML.includes('shot.png') && s.listHTML.includes('[file notes.pdf]') && s.listHTML.includes('Editing <button>')); env.window.attachedImages = [{ name: 'a.pdf' }]; s.composer = ''; Q.enqueueFromComposer(); const c = Q._items()[2]; check('files alone can be queued', Q._items().length === 3 && c.text === ''); check('the draft is the queued text, then what is typed', Q.withQueuedText('still typing') === 'make the header blue\n\nand bold\n\nalso this\n\nstill typing'); check('…whitespace-only typing adds nothing', Q.withQueuedText(' \n') === 'make the header blue\n\nand bold\n\nalso this'); } { const { env, s, Q } = makeEnv(); env.isProcessing = true; s.composer = 'click me'; Q.syncButton(); check('clicking the arrow queues the message', Q.handleSendClick() === true && Q._items().length === 1 && s.composer === ''); Q.syncButton(); check('…the button is Stop again', s.send.label === 'Stop'); check('…and a quick second click does not stop the build', Q.handleSendClick() === true && Q._items().length === 1); env.isProcessing = false; Q.syncButton(); check('when the turn ends the arrow state is dropped', !s.send.classes.has('queueing')); } { const { env, s, Q } = makeEnv(); env.isProcessing = true; Q.syncButton(); check('a click on Stop is left to sendChatMessage', Q.handleSendClick() === false); } { const { Q } = makeEnv(); check('with nothing queued the draft is exactly the typed text', Q.withQueuedText(' hi \n') === ' hi \n'); } // --- A clean finish sends the next one, one per turn ------------------------ { const { env, s, Q, queueText } = makeEnv(); env.isProcessing = true; queueText('first'); queueText('second'); env.isProcessing = false; check('afterTurn with nothing queued sends nothing', makeEnv().Q.afterTurn(true) === false); check('a clean finish sends the next queued message', Q.afterTurn(true) === true && s.sends.length === 1); const first = s.sends[0]; check('…as a queued send (its own text, not the composer)', first.input === null && first.opts.queued && first.opts.queued.text === 'first'); check('…one at a time: the second still waits', Q._items().length === 2 && Q._items()[1].text === 'second'); check('the one setting up is shown as sending, without actions', /queued-message sending[^>]*>[\s\S]*?Sending…<\/span><\/div><\/div>/.test(s.listHTML)); check('…and stays in the draft until the send claims it', Q.withQueuedText('') === 'first\n\nsecond'); check('a send from the composer during that setup joins the queue', queueText('third') && Q._items().length === 3); check('the send claims it when its turn starts', Q.claim(first.opts.queued) === true); check('…then it leaves the draft', Q.withQueuedText('') === 'second\n\nthird'); check('…and a second claim fails', Q.claim(first.opts.queued) === false); env.isProcessing = true; // that turn runs first.resolve(); await tick(); check('a claimed send ending puts nothing back', s.composer === '' && Q._items().length === 2); env.isProcessing = false; Q.afterTurn(true); check('its clean finish sends the next', s.sends.length === 2 && s.sends[1].opts.queued.text === 'second'); } // --- Send now --------------------------------------------------------------- { const { env, s, Q, act, queueText } = makeEnv(); env.isProcessing = true; queueText('one'); queueText('two'); queueText('three'); const two = Q._items()[1]; act('send-now', two.id); check('Send now stops the running turn', s.stops === 1); check('…and sends that message straight away', s.sends.length === 1 && s.sends[0].opts.queued === two); check('…the rest stay queued, in order', Q._items().map(i => i.text).join() === 'two,one,three'); // The stopped turn unwinds while the sent-now message sets up. check('the stopped turn\'s end leaves the queue alone (no Resume banner, nothing back in the box)', Q.afterTurn(false) === true && s.composer === '' && s.toasts.length === 0 && Q._items().length === 3); act('send-now', Q._items()[2].id); check('Send now while one is still setting up puts it next in line', s.sends.length === 1 && s.stops === 1 && Q._items().map(i => i.text).join() === 'two,three,one'); Q.claim(two); env.isProcessing = true; s.sends[0].resolve(); await tick(); env.isProcessing = false; Q.afterTurn(true); check('…and goes when that turn finishes', s.sends[1].opts.queued.text === 'three'); } // --- A queued send called off before its turn starts ------------------------ { const { env, s, Q, queueText } = makeEnv(); env.isProcessing = true; env.window.attachedImages = [{ name: 'q.png' }]; queueText('one'); queueText('two'); env.isProcessing = false; env.window.attachedImages = [{ name: 'typed.png' }]; s.composer = 'typed later'; Q.afterTurn(true); s.sends[0].resolve(); // never claimed: a publish started, the Code view could not save… await tick(); check('a called-off send puts everything back in the composer, oldest first, ahead of the typed text', s.composer === 'one\n\ntwo\n\ntyped later', JSON.stringify(s.composer)); check('…its files go to the front of the tray', env.window.attachedImages.map(a => a.name).join() === 'q.png,typed.png'); check('…and the queue is empty', Q._items().length === 0 && !s.listExists); } // --- Stop overrides what Puter is doing ------------------------------------ { const { env, s, Q, queueText } = makeEnv(); env.isProcessing = true; queueText('first'); queueText('second'); s.composer = 'typing'; // sendChatMessage's Stop branch has stopped the turn by now. env.isProcessing = false; check('Stop with messages queued sends the next one', Q.sendNext() === true && s.sends.length === 1 && s.sends[0].opts.queued.text === 'first'); check('…the rest stay queued and the box is untouched', Q._items().map(i => i.text).join() === 'first,second' && s.composer === 'typing' && s.toasts.length === 0); check('…and the stopped turn\'s end leaves the queue alone (no Resume banner)', Q.afterTurn(false) === true && Q._items().length === 2 && s.composer === 'typing'); check('a second Stop while it sets up sends nothing more', Q.sendNext() === false && s.sends.length === 1); } { const { Q, s } = makeEnv(); check('Stop with nothing queued is a plain stop', Q.sendNext() === false && s.sends.length === 0); } // --- Errors and running out send nothing more ------------------------------- { const { env, s, Q, queueText } = makeEnv(); env.isProcessing = true; env.window._pendingEditTarget = { tag: 'h1' }; queueText('one'); queueText('two'); s.composer = 'typing'; env.isProcessing = false; check('an unclean finish sends nothing', Q.afterTurn(false) === false && s.sends.length === 0); check('…and puts the queue back ahead of the typed text', s.composer === 'one\n\ntwo\n\ntyping', JSON.stringify(s.composer)); check('…bringing the picked element back quietly', s.quietTargets.length === 1 && s.quietTargets[0].tag === 'h1'); check('…and says so', s.toasts.length === 1 && /2 queued messages are back in the box/.test(s.toasts[0])); check('…in plain sentences', !/[:;]/.test(s.toasts[0])); } { const { env, s, Q, queueText } = makeEnv(); env.isProcessing = true; env.window._pendingEditTarget = { tag: 'h1' }; queueText('one'); env.window._pendingEditTarget = { tag: 'footer' }; // picked for the message being typed Q.returnAll({ notify: true }); check('returnAll puts it back', s.composer === 'one' && Q._items().length === 0); check('…without replacing an element picked since', env.window._pendingEditTarget.tag === 'footer' && s.quietTargets.length === 0); check('…saying it is back (one message)', s.toasts[0] === 'Your queued message is back in the box.'); Q.returnAll({ notify: true }); check('…and with nothing queued says nothing', s.toasts.length === 1); // An attachment-only composer send handed back has no text to return. const drafts = s.drafts; s.composer = 'kept'; Q.returnToComposer([{ text: '' }]); check('handing back nothing leaves the composer and the draft alone', s.composer === 'kept' && s.drafts === drafts); } // --- Remove, edit, switch ---------------------------------------------------- { const { env, s, Q, act, queueText } = makeEnv(); env.isProcessing = true; env.window.attachedImages = [{ name: 'gone.png' }]; queueText('drop me'); queueText('fix me'); const [drop, fix] = Q._items(); const drafts = s.drafts; act('remove', drop.id); check('Remove takes the message out', Q._items().length === 1 && Q._items()[0].id === fix.id); check('…frees its files', s.revoked.includes('gone.png')); check('…and saves the draft without it', s.drafts === drafts + 1); s.composer = 'new words'; act('edit', fix.id); check('Edit moves the message back to the composer, ahead of the typed text', Q._items().length === 0 && s.composer === 'fix me\n\nnew words' && !s.listExists); } { const { env, s, Q, queueText } = makeEnv(); env.isProcessing = true; env.window.attachedImages = [{ name: 'x.png' }]; queueText('leaving'); const drafts = s.drafts; Q.reset(); check('a project switch empties the queue', Q._items().length === 0 && !s.listExists); check('…frees its files', s.revoked.includes('x.png')); check('…and leaves the leaving project\'s draft and the composer alone', s.drafts === drafts && s.composer === ''); env.isProcessing = false; check('…and nothing is sent afterwards', Q.afterTurn(true) === false && s.sends.length === 0); } { // A switch during a queued send's setup: the send finds its item gone. const { env, s, Q, queueText } = makeEnv(); env.isProcessing = true; queueText('one'); env.isProcessing = false; Q.afterTurn(true); const item = s.sends[0].opts.queued; Q.reset(); check('a send whose queue was reset cannot claim it', Q.claim(item) === false); s.sends[0].resolve(); await tick(); check('…and nothing comes back into the arriving project\'s composer', s.composer === ''); } // --- The composer draft carries the queue ----------------------------------- { const a = APP.indexOf('// ===== composer-drafts (start) ====='); const b = APP.indexOf('// ===== composer-drafts (end) ====='); const BLOCK = APP.slice(a, b).replace(/^let (_\w+) = [^;]+;/gm, ''); const store = new Map(); const input = { value: 'typed' }; const env = { localStorage: { getItem: k => store.get(k) ?? null, setItem: (k, v) => store.set(k, v), removeItem: k => store.delete(k) }, window: { user: { username: 'nj', is_temp: true }, addEventListener() {}, MessageQueue: { withQueuedText: t => 'queued\n\n' + t, syncButton() {} } }, document: { visibilityState: 'visible', addEventListener() {} }, Date, setTimeout, clearTimeout, console, _draftSeq: 0, _composerRestoredFromKey: null, _cloudDraftPending: null, _cloudDraftChain: Promise.resolve(), _cloudDraftWarned: false, currentChatId: 'chat-1', chatHistory: [{ role: 'system' }], savedChats: [], isProcessing: true, attachedImages: [], autoResizeTextarea() {}, }; env.$ = (sel) => { if (sel === env.document) return { on() { return this; } }; if (sel === '.chat-input-message') return { length: 1, 0: input, val: (v) => (v === undefined ? input.value : (input.value = v)) }; if (sel === '.send') return { prop() { return this; } }; throw new Error('unexpected selector ' + sel); }; const x = new Function('env', 'with (env) {\n' + BLOCK + '\nreturn { saveComposerDraft, composerDraftContext };\n}')(env); check('a turn running in the open chat makes it a project for the draft (no first-turn text under home)', x.composerDraftContext() === 'chat-1'); x.saveComposerDraft(); const saved = JSON.parse(store.get('composerDraft:nj:chat-1') || 'null'); check('saveComposerDraft stores the queued text ahead of the typed text', saved && saved.t === 'queued\n\ntyped', JSON.stringify(saved)); env.isProcessing = false; check('…and with no turn and no history the landing stays home', x.composerDraftContext() === 'home'); } // --- Where the app hooks in -------------------------------------------------- function fnSlice(src, header) { const i = src.indexOf(header); if (i < 0) return ''; const j = src.slice(i + header.length).search(/\n(?:async )?function |\nconst /); return j < 0 ? src.slice(i) : src.slice(i, i + header.length + j); } { const send = fnSlice(APP, 'async function sendChatMessage('); check('Stop stops the turn, then sends the next queued message, or offers Resume when none is queued', /if \(isProcessing\) \{\s*(?:trackBuildAbandoned\('stopped'\);\s*)?stopActiveTurn\(\);[\s\S]{0,300}?if \(window\.MessageQueue\?\.sendNext\?\.\(\)\) return;[\s\S]*?showResumeBanner\(\);\s*return;/.test(send)); const stop = fnSlice(APP, 'function stopActiveTurn('); check('stopActiveTurn marks, aborts and frees the composer, for Stop and Send now alike', /activeTurnInterrupted = true;[\s\S]*abortController\.abort\(\);[\s\S]*resetUIForAbort\(\);/.test(stop) && APP.includes('window.stopActiveTurn = stopActiveTurn;')); check('a send during a queued message\'s setup joins the queue instead of being dropped', /if \(_sendSetupInFlight\) \{\s*if \(userInput == null && chat_input_message == null && !opts\.resume && !opts\.queued\) \{\s*window\.MessageQueue\?\.enqueueFromComposer\?\.\(\);/.test(send)); check('a queued send claims its message where a composer send consumes the composer', /\} else if \(queued && !window\.MessageQueue\?\.claim\?\.\(queued\)\) \{[\s\S]{0,400}?_sendSetupInFlight = false;[\s\S]{0,200}?return;/.test(send) && send.indexOf('window.MessageQueue?.claim?.(queued)') < send.indexOf('isProcessing = true;')); check('a queued send abandoned before its message lands goes back to the composer', /if \(!isResume && \(consumedComposer \|\| queued\) && !turnAppended[^)]*\) \{[\s\S]{0,200}?window\.MessageQueue\?\.returnToComposer\?\.\(\[queued \|\| \{\s*text: messageText,[^}]*\}\]\);/.test(send)); const superseded = send.indexOf('if (supersededInChat) return;'); const after = send.indexOf('window.MessageQueue?.afterTurn?.('); check('the queue moves on only after the turn has fully ended, and only if no newer one started', superseded > 0 && after > superseded && send.indexOf('const sendingQueued = turnChatId === currentChatId && !isProcessing') > superseded); check('…a credits-limited finish counts as unclean', send.includes('afterTurn?.(turnSucceeded && !(context && context.usageLimited))')); check('a turn stopped by Send now offers no Resume', /if \(turnSaveContext\.interrupted && turnChatId === currentChatId && !isProcessing && !sendingQueued\) \{\s*showResumeBanner\(\);/.test(send)); check('the turn start leaves the composer to lockComposerForTurn', send.includes('lockComposerForTurn();') && !send.includes("$('.chat-input-message').prop('disabled', true)")); check('Build Started marks queued sends', send.includes('...(queued && { queued: true })')); const sw = fnSlice(APP, 'function resetChatUIForSwitch('); check('a project switch empties the queue', sw.includes('window.MessageQueue?.reset?.();')); check('the tray hand-off helpers are exposed', APP.includes('window.takeAttachments = takeAttachments;') && APP.includes('window.returnAttachments = returnAttachments;')); } { const keydown = UI.slice(UI.indexOf("$(document).on('keydown', '.chat-input-message'"), UI.indexOf("$(document).on('click', '.app'")); const route = keydown.indexOf('window.MessageQueue?.takesComposerSend?.()'); check('Enter (with or without Cmd/Ctrl) mid-turn queues and never clicks Stop', route > 0 && route < keydown.indexOf("$('.send').click();") && /takesComposerSend\?\.\(\)\) \{\s*window\.MessageQueue\.enqueueFromComposer\(\);\s*return;/.test(keydown) && !keydown.includes('sendNowFromComposer')); check('there is no second button beside Send/Stop', !UI.includes('queue-send') && !CSS.includes('queue-send')); check('the Send button\'s click goes through the queue first', /\$\(document\)\.on\('click', '\.send', async function\(e\) \{[\s\S]{0,200}?if \(window\.MessageQueue\?\.handleSendClick\?\.\(\)\) return;\s*await sendChatMessage\(\);/.test(UI)); check('setEditTarget can restore an element quietly', /window\.setEditTarget = function\(d, \{ quiet = false \} = \{\}\)/.test(UI) && UI.includes('if (quiet) return;')); const lock = fnSlice(HELP, 'function lockComposerForTurn('); check('with the queue on the composer stays open mid-turn', /if \(window\.MessageQueue\?\.enabled\?\.\(\)\) \{[\s\S]*?prop\('disabled', false\)[\s\S]*?return;/.test(lock)); check('the clarify card restores the turn composer the same way', fnSlice(CLARIFY, ' function restoreProcessingComposer(').includes('lockComposerForTurn();') && !/prop\('disabled', true\)/.test(CLARIFY)); check('the flag is on', /messageQueue: true,/.test(HELP)); check('the module loads after app.js', VITE.indexOf("'js/message-queue.js'") > VITE.indexOf("'js/app.js'")); check('npm test runs this file', PKG.scripts.test.includes('scripts/test-message-queue.mjs')); } // --- A stopped turn unwinding under the next one ----------------------------- // Stop with messages queued starts the next turn at once, while the stopped // one may still be uploading or giving up its request. { const send = fnSlice(APP, 'async function sendChatMessage('); check('a stopped turn that throws after a newer one started shows no error card in it', send.includes('if (!shouldStop && !activeTurnInterrupted && turnChatId === currentChatId && turnSeq === _turnSeq) {')); check('…and leaves the newer turn\'s dots alone', /if\(spinner && turnSeq === _turnSeq\)\s*spinner\.remove\(\);/.test(send)); check('its unsent message comes back even when a newer turn has started', send.includes('if (!isResume && (consumedComposer || queued) && !turnAppended && turnChatId === currentChatId) {') && send.includes('if (turnSeq === _turnSeq) clearResumeBanner();')); check('a clarify reply keeps queued messages in the draft', /if \(window\.MessageQueue\?\.withQueuedText\?\.\(''\)\) saveComposerDraft\(\);\s*else clearComposerDraft\(\);/.test(send)); const snap = send.indexOf('const preAuthEditTarget = window._pendingEditTarget || null;'); check('a send takes its picked element when it starts, like its text and files', snap > 0 && snap < send.indexOf('await ensureAuthenticated();') && send.includes('if (consumedComposer && window._pendingEditTarget === preAuthEditTarget) window.clearEditTarget?.();')); } { // clearAllAttachments({ only }) leaves files attached during the send. const a = APP.indexOf('function clearAllAttachments(opts) {'); const b = APP.indexOf('\n}\n', a) + 3; const env = { attachedImages: [], window: {}, revoked: [], timers: 0 }; const fn = new Function('env', `with (env) {\n${APP.slice(a, b)}\nreturn clearAllAttachments;\n}`)(Object.assign(env, { updateAttachmentDisplay() {}, revokeAttachmentURL: (x) => env.revoked.push(x.name), setTimeout: (f) => { env.timers++; f(); }, })); const sent = { name: 'sent.png' }; const later = { name: 'later.png' }; env.attachedImages = [sent, later]; fn({ deferRevoke: true, only: [sent] }); check('a send clears only the files it sent from the tray', env.attachedImages.length === 1 && env.attachedImages[0] === later && env.window.attachedImages === env.attachedImages); check('…revoking only theirs, after the delay', env.revoked.join() === 'sent.png' && env.timers === 1); fn(); check('a plain Clear still empties the tray', env.attachedImages.length === 0 && env.revoked.join() === 'sent.png,later.png'); } // --- Stop is a stop button --------------------------------------------------- check('the running-turn button shows a stop square, not pause', /window\.stop_svg = '