From 4d4d2de1ca7adbf1352abba64b3ce83cf8f21bbe Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 4 Aug 2026 07:29:04 -0400 Subject: [PATCH] =?UTF-8?q?=E2=99=BF=20fix:=20Address=20Codex=20Round=2011?= =?UTF-8?q?=20On=20The=20Queued=20Outbox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three P2 findings, all valid, all local to the outbox group. Merge now stands down on an unresolved empty edit. It reads the queue like every other sender, so folding while the editor was blank carried the words the user had just deleted into the merged message. Clear all is left alone deliberately: its payload lands in the composer, where a reappearing word is visible rather than silently sent. Promotion eligibility is computed in one pass. The per-row prefix scan allocated and walked a slice for every row, and the write-through editor re-renders this list on every keystroke. The collapsed disclosure no longer carries an aria-label. Collapsing unmounts the rows, so the count and next-up preview inside the button are the queue's only description — and an aria-label overrides exactly that text. `aria-expanded` already carries the show/hide state, which is what the label was really conveying. --- .../components/Chat/Input/QueuedOutbox.tsx | 28 +++++++++-- .../__tests__/PendingSteerChips.test.tsx | 50 +++++++++++++++++++ client/src/locales/en/translation.json | 2 - 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/client/src/components/Chat/Input/QueuedOutbox.tsx b/client/src/components/Chat/Input/QueuedOutbox.tsx index c7175d639b..5af6b61880 100644 --- a/client/src/components/Chat/Input/QueuedOutbox.tsx +++ b/client/src/components/Chat/Input/QueuedOutbox.tsx @@ -458,6 +458,15 @@ function QueuedOutboxBase({ const [expanded, setExpanded] = useAtom(queueExpandedFamily(steering.queueKey)); const emptyEditId = useAtomValue(queueEmptyEditFamily(steering.queueKey)); const mergeable = useMemo(() => queued.every(isMergeableQueuedMessage), [queued]); + /** Folding reads the queue, so an unresolved empty edit would carry the words + * the user just deleted into the merged message. Same standdown the senders + * use — the row's own controls, the drain, and the shortcut proxy. */ + const mergeBlockedReason = (() => { + if (!mergeable) { + return localize('com_ui_queue_merge_blocked'); + } + return emptyEditId != null ? localize('com_ui_queue_edit_empty') : undefined; + })(); /** The shortcut's promise is the NEWEST waiting message, which is not the * last array slot once a promotion has reordered the queue — so pick by * stamp. Recovery-bound rows are skipped because steering refuses them @@ -475,6 +484,14 @@ function QueuedOutboxBase({ return newest; }, [queued]); const [next] = queued; + /** Index of the first row a promotion could overtake. Computed once: the + * per-row prefix scan it replaces allocated and walked a slice for every + * row, and the write-through editor re-renders this list on every + * keystroke. */ + const firstOvertakeable = useMemo( + () => queued.findIndex((item) => item.priority !== true), + [queued], + ); const clearAll = useCallback(() => { void (async () => { @@ -528,7 +545,10 @@ function QueuedOutboxBase({