Chat: queue prompts with Cmd/Ctrl+Enter and drag to reorder the queue by shimmyshimmer · Pull Request #8952 · unslothai/unsloth · GitHub
Skip to content

Chat: queue prompts with Cmd/Ctrl+Enter and drag to reorder the queue - #8952

Merged
danielhanchen merged 20 commits into
mainfrom
studio-prompt-queue-mod-enter-and-reorder
Aug 16, 2026
Merged

Chat: queue prompts with Cmd/Ctrl+Enter and drag to reorder the queue#8952
danielhanchen merged 20 commits into
mainfrom
studio-prompt-queue-mod-enter-and-reorder

Conversation

@shimmyshimmer

Copy link
Copy Markdown
Member

The problem

Two gaps in the prompt queue.

You cannot queue on purpose. Queueing only happens as a side effect of submitting while something is already running: handleSubmit checks liveThreadIsRunning || livePromptQueueActive || livePreStreamRunActive and queues in that branch alone. With an idle thread there is no way to stack prompts up front, and no key that means "queue this".

The queue order is fixed. Rows can be edited and removed, but not moved, so a prompt lined up in the wrong order has to be deleted and retyped.

The change

Cmd/Ctrl+Enter queues. It queues whether or not a response is running. With nothing running the queue dispatches it immediately, so it reads like a send, and the next Cmd/Ctrl+Enter lands behind it. Plain Enter is untouched, and so is the existing behaviour of Enter queueing while a run is active.

The chord is claimed in the IME key handler ahead of the plain-Enter submit, since Cmd+Enter carries no Shift and would otherwise fall through to a normal send. It sets a ref that handleSubmit reads once per submit, so a rejected send cannot leave it armed for the next one. Long pasted text that the composer parked in a chip queues through the same path it already used.

Queued rows drag to reorder. The row is the drag source, the handle carries the affordance, and dropping on a row moves the dragged prompt into that slot. ArrowUp and ArrowDown on the handle do the same thing for the keyboard, since HTML5 drag events never fire for keys.

Decisions worth flagging

Only pending rows move. A dispatched item is already on its way out, and a run mid-dispatch is refused outright, so a drag can never race the pump. Nothing may move to or from a slot before the one about to send.

A move across the active slot retargets the pending send. run.index is positional, so reordering can change which item sits in the active slot. That case clears the retry timer and reschedules, exactly as removePromptQueueItem already does for the same reason. A stale scheduled dispatch is harmless on its own, since isActivePromptQueueItem re-checks identity, but leaving the reschedule out would stall the queue when the new occupant has no timer of its own.

The drag payload is text/plain, deliberately not Files. The page-wide dropzone in Thread gates every handler on hasFiles(e), so a row drag never lights up the file-drop affordance.

The index math lives in features/chat/utils/prompt-queue-reorder.ts, next to the other pure queue helpers, rather than inside the 6k-line thread.tsx where nothing is exported and nothing can be tested.

Testing

New tests/prompt-queue-reorder.test.ts, 11 cases: downward drags landing after the target and upward drags landing before it, adjacent swaps in both directions, no items lost and the input left unmutated, self-moves refused, the active-slot boundary enforced from both sides, out-of-range and non-integer indices, and the dispatch-target-changed signal in the three cases that matter including a run that has not dispatched yet.

One case sweeps every from/to pair and asserts the range check agrees with the reorder it guards, so the two cannot drift apart.

Full frontend suite passes at 2713, typecheck is clean, and the production build succeeds.

Not covered

Reordering across two different queue runs is refused. Runs are per target chat, so a cross-run drag would mean moving a prompt to another chat, which the stack has no affordance for.

Cmd/Ctrl+Enter now queues the composer text whether or not a response is
running, so prompts can be stacked up front. Plain Enter is unchanged.

Queued rows are draggable, with ArrowUp/ArrowDown on the row handle as the
keyboard equivalent. Only pending rows move, so a drag cannot race the
dispatcher.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

Rows preventDefault'd every drop, including file drops, and the page dropzone
skips events already prevented, so a file dropped on the queue was silently
lost. Rows now claim only their own private drag type.

Cmd/Ctrl+Shift+Enter queued instead of inserting a newline, since the chord
was matched before the plain-Enter branch that excludes Shift. Shift now
disqualifies it.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

Starting a queue awaits settings hydration before the queue store learns
about it, and nothing else marked the thread as queueing during that gap.
Cmd/Ctrl+Enter on an idle thread therefore left a window where a plain
Enter or a Send click took the normal send path with the same text still
in the composer. The pending queue then started, saw that response
running, and dispatched its own copy, so the prompt went twice.

The reservation now records its thread, and a live one counts towards
livePromptQueueActive. A second submit in that window joins the queue
instead: an identical prompt dedupes on the existing reservation key, and
an edited one queues behind the first.
@shimmyshimmer

Copy link
Copy Markdown
Member Author

chatgpt-codex-connector[bot]

This comment was marked as resolved.

The plain-text path now marks its queue start synchronously, but the
pasted-text path still called startHydratedPromptQueue only after
File.text() resolved. During that read nothing marked the thread as
queueing, so a plain Enter or a Send click took the normal send path and
the read then queued the same text again.

Record the intent before the read and drop it when the read settles, so
a submit during it joins the queue instead.
@shimmyshimmer

Copy link
Copy Markdown
Member Author

Correct, and it is the same window the previous fix closed on the plain-text path, left open on this one. queuePastedTextPrompt only reaches startHydratedPromptQueue inside the .then of Promise.all(files.map((file) => file.text())), so for the length of that read nothing marked the thread as queueing.

Fixed in 8a415a0. The intent is recorded in a small pending list before the read begins and dropped when it settles, in a finally so a failed read cannot leave the composer stuck looking like it is queueing. livePromptQueueActive consults that list alongside the reservation map, through the same hasPendingPromptQueueStart predicate, so a submit during the read joins the queue rather than taking the send path.

The entry is pushed after the early returns, so a composer with no pasted-text attachment never registers anything. Tracking is per thread for the same reason as the reservation map: the composer survives thread switches, so another thread's in-flight read must not make this one look busy.

Tests cover concurrent reads on two threads and one finishing while the other stays counted. Suite is at 2726, tsc -b clean, no new biome errors.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

A file read that precedes a queue start sat outside every invalidation
path: Clear all cancelled the reservations and advanced the history
boundary, but the read carried on and queued its prompt afterwards,
recreating the chat. The read now registers with the same cancel and
boundary mechanism the reservations use.

Recording the read as an active queue also routed a second submit into
the queue branch, which started another read of the same attachment and
queued a duplicate once the first reservation had been released. Reads
are now keyed by thread, text and attachments, so the second submit
joins the read already in flight.
@shimmyshimmer

Copy link
Copy Markdown
Member Author

Both new points are right, and both are fixed in 2a220ce.

Cancel pending paste reads when clearing queues. Correct, and the gap was wider than the cancelled field. The pending read sat outside every invalidation path at once: cancelPendingPromptQueueFactoriesForStop only walked the reservation map, cancelled was never read, and createPromptQueueTarget captures chatHistoryClearBoundary at its own call time, which is after the read. clearAllChats advances that boundary and then dispatches requestPromptQueueStop() with no thread ids, so it cancels every reservation, but a read in flight carried on and queued its prompt afterwards, recreating a chat the user had just cleared.

The read now registers the same way a reservation does. It carries temporary, so the stop helper can filter it, and it captures both boundaries before the read begins. The stop listener runs the same cancel helper over it, and the resolve path bails on cancelled or on a history boundary that has moved. The local model boundary is threaded into startHydratedPromptQueue as the captured generation, so the check happens where the target is known but against the generation from before the read rather than after it.

Avoid starting another read for an already-pending paste. Correct, and it is the cost of the previous fix. Recording the read as an active queue is what stops a second submit taking the send path, but it routes that submit into the queue branch, which called queuePastedTextPrompt again for the same unchanged attachment. Two reservations with the same key dedupe, so the duplicate needs the first read to have finished its hydration before the second read resolves, exactly as described.

Reads are now keyed and held in a map, so a submit during a read joins the one already in flight and reports handled rather than starting another.

One deliberate choice there: the wait mode is not part of the key. It is recomputed per submit, and if a run starts or ends mid-read the two submits compute different values, which would split one prompt across two keys and bring the duplicate straight back. What identifies the prompt is the thread, the text and the attachments it is assembled from, so those are the key and the first read's wait mode wins.

pastedTextQueueKey is in features/chat/utils/prompt-queue-input.ts with tests for a stable key, a differing thread, text or attachment, reordered attachments, and a null thread not colliding with a named one. Suite is at 2730, tsc -p tsconfig.app.json clean, and eslint on the touched files reports the same counts as the branch tip did before the change.

Scope worth stating: the key is unit tested, but the cancel wiring is verified by typecheck and inspection rather than by driving the real composer, since reproducing the window needs the full chat UI with a Clear all landing inside a File.text() call.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

The reservation took its settings epoch and incognito flag after the
file read, so a setting changed during the read became the baseline
instead of aborting the queue, unlike every other pending start. The
read now captures all of them up front and the reservation uses those.
@shimmyshimmer

Copy link
Copy Markdown
Member Author

Right, and it is a gap I introduced with the previous commit rather than one that was already there. That commit captured the history clear boundary and the local model boundary before the read and threaded the latter into the reservation, but left the settings epoch and the incognito flag to be taken afterwards. So a queue-relevant setting changed during the read became the reservation's own baseline, and shouldAbortPendingQueueForSettingsChange had nothing to compare against, while an ordinary pending start in the same situation aborts.

Fixed in 05ba402. The read now captures every baseline the reservation would otherwise take: the local model boundary generation, the settings epoch, the incognito flag and the history clear generation. The optional argument on startHydratedPromptQueue is now the whole captured set rather than one generation, and the reservation prefers it when present, so all three checks run against the state as it was when the user made the queue gesture.

Suite is at 2730, tsc -p tsconfig.app.json clean, and eslint on thread.tsx reports the same counts as the branch tip did before these changes.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

A read whose captured settings had already gone stale still absorbed
the next submit as a duplicate, and then aborted itself, so neither
gesture queued anything. The dedupe now only absorbs a read that is
still going to start; a stale one is replaced, and it drops out because
it no longer owns its key. An abort also says so rather than being
silent.
@shimmyshimmer

Copy link
Copy Markdown
Member Author

Right, and it is the cost of the dedupe from the previous commit: the read absorbed the retry as a duplicate and then aborted itself, so the two gestures cancelled each other out and nothing queued.

Fixed in 90fc433. The dedupe now only absorbs a read that is still going to start. pendingPastedTextReadIsStale runs the checks that need no queue target, the cancel flag, the history clear boundary and shouldAbortPendingQueueForSettingsChange, and a read that fails any of them no longer swallows the retry. The retry takes the key, so the stale read still resolves but drops out on the ownership check and only the new one queues. The model boundary check stays with the reservation, where the target is known and usesLocalModel can be evaluated, so the dedupe cannot be stricter than the abort rule and reintroduce the duplicate.

The abort path also no longer passes undefined for onAborted. A queue that is invalidated after the read now says so, matching what the saved-list path already does, so a gesture that queues nothing is never silent.

Suite is at 2730, tsc -p tsconfig.app.json clean, and eslint on thread.tsx reports the same counts as before the change.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

The pasted-text read had this fixed; the reservation itself did not. A
retry with the same prompt hit the duplicate check, the original then
aborted on its stale settings epoch, and neither gesture queued. The
staleness predicate now covers both, and a reservation that is already
invalid is replaced rather than turning the retry away. A superseded
start no longer reports an abort, since the one that replaced it is
still going.
@shimmyshimmer

Copy link
Copy Markdown
Member Author

Right, and it is the same defect one level up: the previous commit fixed it for the pasted-text read and left the reservation itself turning retries away.

Fixed in ba230a0. pendingPastedTextReadIsStale is now pendingQueueStartIsStale and covers both, with the history clear generation optional since a reservation captures that inside createPromptQueueTarget rather than up front. The duplicate check only refuses a retry when the reservation it collides with is still going to start; an already-invalid one is replaced, and it drops out on the get(key) === reservation check it already had.

The model boundary check deliberately stays out of the predicate. It needs usesLocalModel from the target, so it can only run once the target exists. Keeping it there means the predicate can never be stricter than the abort rule, which is what would let two starts through for the same prompt.

One thing that fell out of this: a superseded reservation used to run onAborted, which after the previous commit would have shown "Pasted text was not queued" even though the retry was about to queue it, and would have reopened the saved-list dialog for the same reason. onAborted now fires only when the reservation is still the one holding the key, so a replacement is quiet and a real abort still reports.

Suite is at 2730, tsc -p tsconfig.app.json clean, eslint unchanged from the branch tip.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba230a0838

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +3151 to +3157
const chatState = useChatRuntimeStore.getState();
return shouldAbortPendingQueueForSettingsChange({
capturedEpoch: pending.queuedSettingsEpoch,
currentEpoch: chatState.queuedSettingsEpoch,
capturedTemporary: pending.temporary,
currentTemporary: chatState.incognito,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Permit retries after a local-model boundary change

When a model load or unload advances localPromptQueueModelBoundary while createPromptQueueTarget() or a pasted-text read is pending, this predicate can still classify the reservation as fresh because it checks only the settings/history state. A subsequent Enter is therefore consumed as a duplicate, while the original reservation later fails modelBoundaryInvalidated, leaving the prompt unsent. This occurs when the boundary advances before queuedSettingsEpoch changes; the retry path needs to recognize that invalidated local reservation or otherwise avoid absorbing the newer submission.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The analysis is right and the fix as asked is not, so this one stays as it is. Adding the model boundary to pendingQueueStartIsStale would make that predicate stricter than the reservation's own check: the predicate cannot know usesLocalModel, which is only known once createPromptQueueTarget resolves, so on an external-model target it would call a reservation stale that is still going to start, and the retry would open a second queue for the same prompt. Trading a transient no-op for a duplicate send is the wrong direction. The residual behaviour is bounded: the composer is only cleared in onStarted, so the text is still there and the next press queues normally once the reservation has resolved and left the map.

@danielhanchen

Copy link
Copy Markdown
Member

Shot this from two isolated installs, BEFORE at the merge base 6f443b5cc and AFTER at the head ba230a083, to see what the change looks like on screen.

One flow covers both halves of the PR. Four prompts are submitted into an idle thread with Ctrl+Enter, then the bottom queued row is walked to the top with ArrowUp on its handle. Model is unsloth/Qwen3-4B-Thinking-2507-GGUF Q4_K_M on both sides.

prompt queue reorder

Both halves are holding the same three rows before anything moves, alpha / beta / gamma, which is the control. After the same two ArrowUp presses the base still reads alpha, beta, gamma and the head reads gamma, alpha, beta.

BEFORE 6f443b5cc AFTER ba230a083
reorder handles on the rows 0 3
order after two ArrowUp presses alpha, beta, gamma gamma, alpha, beta
queue header after the four chords Prompt queue, 0 of 3 Prompt queue, 1 of 4

The header is where the chord shows up, and it is worth saying why it is a number rather than a picture. getPromptQueueUIItemsForRun skips item.dispatched, so a dispatched item leaves the stack, and on an idle thread the chorded prompt dispatches immediately. Both stacks therefore end up showing the same rows, and a row's own label counts the rows on screen rather than the run. Only the header still counts the dispatched one: 0 of 3 on the base means the first prompt was sent outside any queue and only the three that landed while it streamed were queued, against 1 of 4 on the head where all four are one run.

Two things I got wrong first, in case they are useful for the next one of these.

Photographing the chord on its own does not work. My first plan gave it a pair of its own with the model unloaded, so the thread would stay idle throughout. A send with nothing loaded makes Studio auto-load the 270M default, and that load itself reads as a run, so the two sides queue identically.

The queue drains at the speed of the model, and the first run photographed the base holding three rows against the head holding two, purely because the head's opening prompt finished sooner. Two different queues, and it reads as a difference this PR made. The scene now asserts the settled rows are the three expected prompts and retries the flow otherwise. With the 270M default the whole stack is gone before any shot lands, on either side.

One conflict, in the composer's hook block: main added the
pastedTextMinChars preference read (#8963) and this branch added the
Cmd/Ctrl+Enter ref and its handler at the same spot. Both are kept.

Nothing else in thread.tsx conflicted, and the paste threshold does not
change what the queue sees: a long paste still arrives as an attachment
that isPastedTextFile recognises, which is what canQueuePastedTextPrompt
gates on.
`npm test` runs under node --experimental-strip-types, whose ESM loader
does not resolve an extensionless specifier, so every test that reaches
the preferences store died on ERR_MODULE_NOT_FOUND. It fails the same way
on main at 2cf7a28, so it is not something this branch introduced, but
it takes the branch's own suite down with it. Vite resolves it either
way; the rest of the files on the test path already carry the extension.
Two hardening fixes, both found by testing the chord across engines
rather than by a failure in the wild.

Windows reports AltGr as Ctrl+Alt on any layout the browser does not
recognise as carrying a real AltGraph modifier, which the UI Events spec
allows for explicitly ("some operating systems simulate the AltGraph
modifier key with the combination of the Alt and Control modifier keys").
On those layouts AltGr is held for everyday characters, so the chord
would fire on a keypress meant as something else. Alt now disqualifies
it, the way Shift already does. There is no Ctrl+Alt+Enter binding here
worth keeping.

The chord also armed its ref before knowing there was a submit to reach.
`event.currentTarget.form?.requestSubmit()` is a no-op when the textarea
sits outside a form, and requestSubmit is missing entirely on Safari
before 16, so in both cases the ref stayed armed and the NEXT submit --
an ordinary Enter, possibly minutes later -- queued instead of sending.
It is now armed only when the call is actually going to happen, and
disarmed if it throws.

Tests: an edge file for the predicates (numpad Enter, every platform's
modifier combination, AltGr, DOMStringList types, a Map's one-shot
values iterator, JSON-lookalike prompt text, NFC against NFD), and an
exhaustive sweep of the reorder math over every queue length, from/to
pair and active slot up to seven items -- permutation preserved, the
spent head never moves, the range check and the reorder never disagree,
a move and its reverse restore the queue, the input is never mutated,
and the dispatch-changed flag agrees with the slot it reports on.

Suite 2900 passing, tsc -b clean, and eslint on thread.tsx reports the
same 50 pre-existing errors as it did before the change.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

@danielhanchen

Copy link
Copy Markdown
Member

Went through this one properly before merging: what changes for a user, whether anything else moves, and how it behaves outside Chrome on Linux. Three commits pushed to the branch, 27dcc075d.

Before and after

Before, queueing was only ever a side effect. handleSubmit queued in one branch, the one guarded by liveThreadIsRunning || livePromptQueueActive || livePreStreamRunActive, so a prompt joined the queue only because a response happened to be running when you pressed Enter. On an idle thread there was no key that meant "stack this", and once rows existed the order was fixed: edit and remove, no move.

After, Cmd/Ctrl+Enter queues whether or not something is running, and a queued row moves by drag or by ArrowUp/ArrowDown on its handle.

The gap is real rather than cosmetic, and the measurement that shows it is the queue's own header, since it counts the item already dispatched. Four prompts submitted in quick succession into an idle thread:

merge base 6f443b5cc head 27dcc075d
Ctrl+Enter x4 Prompt queue, 0 of 3 Prompt queue, 1 of 4
Cmd+Enter x4 Prompt queue, 0 of 3 Prompt queue, 1 of 4

0 of 3 is the old behaviour: the first prompt went out on its own and only the three that landed while it streamed were queued.

Does it break anything

No, and the parts that must not move were measured against the merge base rather than asserted. Same three engines, same script:

base head
plain Enter sends 1 message, composer cleared identical
Shift+Enter one\ntwo identical
Ctrl+Shift+Enter inserts nothing, does not send identical
Ctrl+Alt+Enter x4 Prompt queue, 0 of 3 identical
five submits of one unchanged prompt 1 user message identical

There is no hardware path to worry about here. The diff is five files, all under studio/frontend, and it touches no Python, no API route, no localStorage key and no persisted schema, so there is nothing for a GPU, an accelerator or an OS to bite on. The queue lives in a module-level Map and dies with the tab.

Old installs are fine for the same reason, and I checked rather than reasoned: I installed this branch in place over an existing merge-base install carrying real chats. The old password still logs in, so auth.db came through untouched, all 21 threads read back through the upgraded backend, no row count went down, and all ten behaviour checks pass on the upgraded home.

Cross-browser

Driven against a real built Studio in Chromium 151, Firefox 153 and WebKit 26.5, ten checks each, thirty of thirty passing and no difference between the engines: both chords, the two newline combinations, plain Enter, auto-repeat, a drag driven through a real DataTransfer, the keyboard handle twice on the same element, and a file dropped onto a queue row.

Two of those are worth calling out.

The custom drag type survives everywhere. All three report application/x-unsloth-prompt-queue-item back verbatim in types during dragover and through getData on drop. WebKit filters custom drag data by origin, which is a real restriction but not one this can hit, since the drag starts and ends in the same document.

The keyboard reorder keeps focus. This is the one I expected to fail: the HTML spec blanks the document's focused area when a focused node is removed, and moving a keyed row is a remove plus an insert. Measured in all three engines, in both directions, the handle stays focused and the second ArrowUp lands. Worth knowing it holds by React's choice of which row to move rather than by guarantee, so it is a thing to re-check if the stack's markup changes.

What I changed

AltGr no longer fires the chord. Windows reports AltGr as Ctrl+Alt on any layout the browser does not recognise as carrying a real AltGraph modifier, which the UI Events spec allows for explicitly. On those layouts AltGr is held for everyday characters, so the chord could fire on a keypress meant as something else. Alt now disqualifies it, the way Shift already did. The control row above is that fix: Ctrl+Alt+Enter now behaves exactly as it does on the merge base.

The chord no longer arms itself when there is nothing to submit. event.currentTarget.form?.requestSubmit() is a no-op when the textarea sits outside a form, and requestSubmit is missing entirely on Safari before 16. In both cases forceQueueRef stayed armed and the next submit, an ordinary Enter possibly minutes later, queued instead of sending. It is armed only when the call is going to happen now, and disarmed if it throws.

The branch is merged with main. One conflict, in the composer's hook block, where main added the pastedTextMinChars read from #8963 and this branch added the chord ref at the same spot. Both kept. The paste threshold does not change what the queue sees: a long paste still arrives as an attachment isPastedTextFile recognises, which is what canQueuePastedTextPrompt gates on.

One thing that was not this branch's fault. chat-preferences-store.ts imports ../utils/pasted-text without an extension, which node's type-stripping loader cannot resolve, so every test reaching the preferences store died on ERR_MODULE_NOT_FOUND. It fails the same way on main at 2cf7a2888. Fixed here because it takes this branch's suite down with it, and the rest of the files on the test path already carry the extension.

Tests added

An edge file for the predicates and an exhaustive sweep of the reorder math.

The sweep runs every queue length up to seven against every from/to pair and every active slot, and asserts the invariants a queue engine depends on: the result is always a permutation with nothing lost or duplicated, nothing at or before the dispatching slot ever moves, the range check and the reorder never disagree, a move and its reverse restore the queue, the caller's array is never mutated, a frozen array works, and the dispatch-changed flag agrees with the slot it reports on. The reorder math came through all of it unchanged, which is the main thing I wanted to know before trusting a drag to retarget a pending send.

The edge file covers what the DOM hands us on platforms this box cannot run: numpad Enter, every platform's modifier combination, the AltGr shape, a DOMStringList rather than the array Chromium returns, a Map's one-shot values iterator, prompt text that looks like the key's own JSON encoding, and NFC against NFD.

Suite 2900 passing, tsc -b clean, eslint on thread.tsx reports the same 50 pre-existing errors it did before.

Not covered

Real Safari, real Windows and real macOS. WebKit 26.5 under Playwright is the closest stand-in for Safari that a Linux box can run, and the Cmd and AltGr chords were synthesised as key events rather than typed on the hardware. The drags are real DragEvents carrying a real DataTransfer, which is what the app receives, but the browser's own drag initiation from a mouse press is only exercised in Chromium. Cross-run drags, refused by the match.run !== target.run guard, are covered by reading the code rather than by a test.

A chord pressed while a stored chat's own settings are still loading was
read and cleared at the top of handleSubmit, then parked by the
threadScopedSettingsPending branch as an ordinary pending send. The
release effect calls sendReservedComposer directly rather than returning
through handleSubmit, so the prompt went out as a plain send and the one
shortcut whose whole point is stacking silently did nothing.

The intent now rides with the parked send and the release queues it.
Three details worth stating.

queueComposerText is hoisted out of handleSubmit, because the release
effect cannot reach into that closure. It reads the live composer rather
than the rendered text: at release it runs from an effect, where the
rendered copy can be a commit behind.

The wait mode is recomputed at release rather than carried from the
parked submit. A run can start while the settings load, and a queue that
ignored it would dispatch on top of the response already streaming.

The parked intent is cleared by cancelQueuedSend and on unmount, next to
pendingSendRef, so a cancelled or abandoned park cannot leave it armed
for a later send.

Suite 2900 passing, tsc -b clean, and eslint on thread.tsx reports the
same 50 pre-existing errors as before the change.
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 16, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 16, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 16, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 16, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 16, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 16, 2026
@unslothai unslothai deleted a comment from shimmyshimmer Aug 16, 2026
chatgpt-codex-connector[bot]

This comment was marked as resolved.

Two from the same round, both on the pasted-text branch.

A read that resolves after the composer moved to another chat could
queue into that chat. The queue target is anchored where
createPromptQueueTarget is CALLED, and on this path that is after
File.text() rather than before it, so a switch mid-read leaves the
target aiming at the chat now on screen: initialRunningThreadIds is
built from the live thread list item, the post-await identity check
passes against it, and thread A's prompt dispatches into thread B. The
read now records which chat the composer was showing when it began and
drops itself if that has changed, the same composerIdentityRef check the
dictation send already uses.

Releasing a parked chord always queued the composer text, so a pasted
attachment was left behind: an attachment-only prompt queued nothing at
all, and one with both queued only the typed half. The release now takes
the same two branches handleSubmit takes, in the same order, and falls
through to an ordinary send when neither can queue, which is what this
path did before the intent was carried at all.

Suite 2900 passing, tsc -b clean, eslint on thread.tsx back to the same
50 pre-existing errors.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 5c936a2bfe

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Post-convergence comment pass over the files this PR touches. Comments
only: the reasoning each one carries is kept, the words around it are
not. Nothing pre-existing in thread.tsx was rewritten, so the diff stays
readable as this PR's own work.

comment_tools.py check reports 4/4 code-unchanged, suite 2900 passing.
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 16, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 16, 2026
chatgpt-codex-connector[bot]

This comment was marked as resolved.

The body is recorded beside the File when the paste is attached, so the
queue can stack it in the gesture itself instead of a task later. A
gesture that awaited landed behind any later one that did not, putting
the two prompts in the queue the wrong way round.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 517dade9f9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

main's composer send guard (#8849) and this branch both edit the
composer keydown path and every site that clears the composer after a
queue. Both kept: the chord and the guard are passed to the same input
hook, and the two hoisted queue helpers arm the guard where main armed
it inline.
Both new modules were reached by path, which the import rule refuses and
which was the only lint finding this branch added on top of main.
One keydown handler now asks both predicates, so no key may be claimed
by both, and the two AltGr rules have to agree.
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 16, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 054adf1405

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review


P3 Badge Escape the NUL so the test remains text

The literal NUL in this template string causes Git to classify the entire TypeScript test as binary (the commit is rendered as a GIT binary patch), so future changes cannot be reviewed or merged as normal line diffs. Use an escaped source representation such as \0 while preserving the same runtime test value.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@danielhanchen
danielhanchen merged commit e4d4eb6 into main Aug 16, 2026
36 of 39 checks passed
@danielhanchen
danielhanchen deleted the studio-prompt-queue-mod-enter-and-reorder branch August 16, 2026 14:37
danielhanchen added a commit that referenced this pull request Aug 17, 2026
…9026)

* Repair the prompt-queue contract test against the queueing refactor

Repo tests (CPU) is red on main. test_composer_only_queues_behind_the
_current_chat greps thread.tsx for code that #8952 moved or replaced, so
it fails on any branch regardless of what that branch changes. It is
currently red on #8935, #8964, #8980 and #8983 for this reason alone.

The behaviour it guards is intact, and in two of the three cases the
code that replaced it is stronger than what the test still asserted, so
the assertions are repointed rather than dropped:

- Queueing moved out of handleSubmit into an extracted queueComposerText,
  so the Cmd/Ctrl+Enter path could share it. Assert the delegation in
  handleSubmit and the queueing inside queueComposerText, which keeps
  this a contract on behaviour rather than on where the code sits.

- promptQueueStartPendingRef.current.has(reservationKey) became a
  .get(reservationKey) === identity comparison. A reservation can be
  replaced between the start and the callback, and acting on the
  successor would dispatch the wrong prompt; presence alone never
  caught that.

- temporary: useChatRuntimeStore.getState().incognito became
  temporary: incognitoAtQueueStart, captured when the queue starts
  instead of read live at dispatch. A chat toggled out of temporary
  mid-queue must not have its queued prompts persisted.

Each rewritten assertion was checked against a deliberately broken tree
rather than assumed to discriminate. Removing the delegation, swapping
the identity check back to presence, reading temporary live again, and
stopping queueComposerText from queueing each fail the file.

tests/studio 11 passed for this file, whole directory green.

* Read the identity check out of the dispatch guard, not the whole file

Reported on this PR and correct. The assertion searched thread.tsx for
promptQueueStartPendingRef.current.get(reservationKey) === anywhere, and the
abort and cleanup branches beside the dispatch carry the same comparison. The
dispatch guard could regress to .has(reservationKey) on its own, which is exactly
the bug this assertion exists to catch, and the test would still pass on its
neighbours.

It now slices the if condition that guards the startPromptQueue call and asserts
the identity comparison there, with .has excluded from that guard. All three
comparisons are still required by count, because the other two are load-bearing
too: abort without it reports the successor's start as this one's failure, and
cleanup without it deletes the successor's entry.

11 passed. Rewriting the dispatch guard to .has fails it; it passed before.

---------

Co-authored-by: danielhanchen <unslothshared@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants