diff --git a/LAWS/CHAT.md b/LAWS/CHAT.md
index 9216c0628..7aa0d095b 100644
--- a/LAWS/CHAT.md
+++ b/LAWS/CHAT.md
@@ -30,3 +30,5 @@
- A message that is not first in the queue MUST NOT steer the session.
- A steering result MUST affect only the message that produced it.
+- While the session is running, a send shortcut with an empty composer MAY steer the first queued message.
+- A send shortcut MUST NOT steer a queued message while the composer holds draft content or a queued message is being edited.
diff --git a/src/features/chat/ui/ChatInput.tsx b/src/features/chat/ui/ChatInput.tsx
index f14b3622f..6480acc7e 100644
--- a/src/features/chat/ui/ChatInput.tsx
+++ b/src/features/chat/ui/ChatInput.tsx
@@ -478,10 +478,12 @@ export function ChatInput({
};
}, [scheduleResizeTextarea, surface]);
- const visibleQueuedMessages = (
+ const allQueuedMessages =
queuedMessages ??
- (queuedMessage ? [{ recordId: "legacy", payload: queuedMessage }] : [])
- ).filter(({ payload }) => payload.showInComposer !== false);
+ (queuedMessage ? [{ recordId: "legacy", payload: queuedMessage }] : []);
+ const visibleQueuedMessages = allQueuedMessages.filter(
+ ({ payload }) => payload.showInComposer !== false,
+ );
// A record being edited lives in the composer, so its pill is hidden to
// avoid showing the same message both queued and in the composer. Queue
// positions (head-only actions) still come from the unfiltered list.
@@ -504,6 +506,30 @@ export function ChatInput({
canSteerMessage &&
visibleQueuedMessages.length === 0 &&
Boolean(onSteerMessage);
+ // Steering acts on the true queue head, so it is only offered when that
+ // head is also the message the user can see. In practice hidden records
+ // (reliable startup handoffs) cannot coexist with an active run today;
+ // this is a tripwire so a future longer-lived hidden record makes
+ // steering go inert instead of steering something off-screen.
+ const queuedHeadIsVisible =
+ allQueuedMessages.length > 0 &&
+ allQueuedMessages[0].payload.showInComposer !== false;
+ // With an empty composer, the send shortcut steers the first queued
+ // message instead of no-oping — the double-enter flow (enter queues,
+ // enter again steers). Draft content keeps the shortcut on the draft so
+ // it can never discard or bypass what the user is composing, and an
+ // in-progress queue edit keeps the shortcut inert because the edited
+ // message lives in the composer, not the queue.
+ const canSteerQueuedMessageWithShortcut =
+ !hasDraftContent &&
+ !attachmentWorkPending &&
+ !disabled &&
+ !sendDisabled &&
+ isStreaming &&
+ canSteerQueuedMessage &&
+ editingQueuedRecordId === null &&
+ queuedHeadIsVisible &&
+ Boolean(onSteerQueuedMessage);
const effectivePersonaId = editingQueuedPersona
? editingQueuedPersona.kind === "persona"
@@ -1148,6 +1174,10 @@ export function ChatInput({
void handleSteerCurrentMessage();
return;
}
+ if (canSteerQueuedMessageWithShortcut) {
+ handleSteerQueuedMessage();
+ return;
+ }
}
void handleSend();
return;
@@ -1183,6 +1213,10 @@ export function ChatInput({
void handleSteerCurrentMessage();
return;
}
+ if (canSteerQueuedMessageWithShortcut) {
+ handleSteerQueuedMessage();
+ return;
+ }
}
void handleSend();
@@ -1636,7 +1670,10 @@ export function ChatInput({
{payload.text}
- {index === 0 && isStreaming && canSteerQueuedMessage ? (
+ {index === 0 &&
+ isStreaming &&
+ canSteerQueuedMessage &&
+ queuedHeadIsVisible ? (