feat(web): add configurable queue/steer follow-up behavior#1479
feat(web): add configurable queue/steer follow-up behavior#1479leonardoxr wants to merge 2 commits intopingdotgg:mainfrom
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| return () => { | ||
| document.removeEventListener("pointerdown", handlePointerDown); | ||
| }; | ||
| }, []); |
There was a problem hiding this comment.
React hooks called after conditional early return
High Severity
ComposerQueuedFollowUpsPanel calls useState, useRef, and useEffect after a conditional early return on line 73. This violates React's Rules of Hooks — hooks must be called unconditionally in the same order on every render. When queuedFollowUps transitions between empty and non-empty (e.g., after auto-dispatch or deleting all items), React will throw a "Rendered fewer hooks than expected" error and crash the component.
|
I want this behavior to be server side. I shouldn't need to keep my client open for the queing to work |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 11ec9d73f3
ℹ️ 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".
| ) : phase === "running" ? ( | ||
| <button | ||
| type="button" | ||
| className="flex size-8 cursor-pointer items-center justify-center rounded-full bg-rose-500/90 text-white transition-all duration-150 hover:bg-rose-500 hover:scale-105 sm:h-8 sm:w-8" | ||
| onClick={() => void onInterrupt()} | ||
| aria-label="Stop generation" | ||
| > | ||
| <svg | ||
| width="12" | ||
| height="12" | ||
| viewBox="0 0 12 12" | ||
| fill="currentColor" | ||
| aria-hidden="true" | ||
| composerSendState.hasSendableContent ? ( | ||
| <Tooltip> |
There was a problem hiding this comment.
Keep interrupt action available during active runs
When phase === "running", the stop button is now only rendered in the composerSendState.hasSendableContent === false branch. As soon as a user types a follow-up (or adds an attachment), the interrupt control disappears, and there is no alternate thread.turn.interrupt trigger in this component. This prevents stopping runaway/expensive turns while drafting a follow-up, so interrupt should remain accessible regardless of composer content.
Useful? React with 👍 / 👎.
| enqueueQueuedFollowUp(activeThread.id, followUpSnapshot); | ||
| promptRef.current = ""; | ||
| clearComposerDraftContent(activeThread.id); | ||
| setComposerHighlightedItemId(null); |
There was a problem hiding this comment.
Revoke blob previews after queue/steer image follow-ups
In the running follow-up path, images are first converted to persisted dataUrls and then the composer is cleared via clearComposerDraftContent. That clear path intentionally does not revoke blob: preview URLs, and unlike normal sends these queue/steer flows do not retain those blob previews in optimistic-message handoff state. Result: queuing/steering image follow-ups leaks object URLs (and memory) until unload.
Useful? React with 👍 / 👎.
|
| ); | ||
| } | ||
|
|
||
| export const ComposerQueuedFollowUpsPanel = memo(function ComposerQueuedFollowUpsPanel({ |
There was a problem hiding this comment.
🔴 Critical chat/ComposerQueuedFollowUpsPanel.tsx:60
React hooks (useState, useRef, useEffect) are called after an early return when queuedFollowUps.length === 0, violating the Rules of Hooks. When the array transitions from non-empty to empty, React throws "Rendered fewer hooks than expected" because the component previously called hooks but now returns early without calling them.
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/components/chat/ComposerQueuedFollowUpsPanel.tsx around line 60:
React hooks (`useState`, `useRef`, `useEffect`) are called after an early return when `queuedFollowUps.length === 0`, violating the Rules of Hooks. When the array transitions from non-empty to empty, React throws "Rendered fewer hooks than expected" because the component previously called hooks but now returns early without calling them.
Evidence trail:
ComposerQueuedFollowUpsPanel.tsx lines 70-72 (early return when `queuedFollowUps.length === 0`), lines 74-82 (`useState` and `useRef` hooks called after early return), lines 84-93 (`useEffect` hook called after early return). React Rules of Hooks documentation: https://react.dev/reference/rules/rules-of-hooks
|
@juliusmarminge A question about the Steer behavior. Should the steer wait for the AI to finish its "step/thinking/action" before sending(like claude code does) or should it interrupt indeed. Or, should it also be an option? Like Queue / Steer / "Force Steer" |


Closes #1462
Summary
Adds explicit follow-up behavior while a thread is already running.
Users can now choose a global
Follow-up behaviorsetting:Steer: send the follow-up as guidance for the active runQueue: hold the follow-up and auto-send it after the current run settlesThe composer also supports a one-off opposite behavior shortcut for a single message.
What changed
followUpBehaviorto client settings with a default ofsteerQueuevsSteerCtrl+Shift+EnterCmd+Shift+EnterWhy
This makes follow-up delivery explicit and predictable while a run is active, which is the core problem described in #1462.
Verification
bun fmtbun lintbun typecheckbun run test -- src/composerDraftStore.test.tsbun run test:browser -- src/components/ChatView.browser.tsx --testNamePattern "queued|follow-up"Media
Settings
Before
After
Before/after comparison
Preview
Note
Medium Risk
Updates the chat composer send path and introduces persisted per-thread queued follow-ups with auto-dispatch, which can affect message ordering/visibility while a turn is running. Risk is moderated by added unit + browser coverage but still touches core chat UX/state handling.
Overview
Adds a new configurable follow-up behavior (default
steer) that controls what happens when the user submits while a thread is alreadyrunning: either steer the active run immediately or queue the follow-up for later via a one-off opposite-behavior shortcut.Implements per-thread queued follow-up persistence and management in
composerDraftStore(new storage version, edit/requeue semantics, reorder/move/delete/restore, and last-send error tracking), plus a newComposerQueuedFollowUpsPanelUI to steer/edit/delete and drag-reorder queued items.Refactors ChatView turn submission to dispatch from a snapshot (reused for plan follow-ups), hides “steer” follow-up messages from the visible timeline, and auto-dispatches the queue head once the thread becomes sendable again; adds settings UI for
followUpBehaviorand extends tests across logic/store and browser interactions.Written by Cursor Bugbot for commit 11ec9d7. This will update automatically on new commits. Configure here.
Note
Add configurable queue/steer follow-up behavior in ChatView
followUpBehaviorsetting ('steer' default, 'queue' optional) toClientSettingsSchemaand exposes a selector in the chat settings UI.Ctrl+Shift+Enter/Cmd+Shift+Entershortcut inverts the behavior once.queuedFollowUpsByThreadIdmap in the composer draft store, with queue operations (enqueue, reorder, edit/restore, steer, delete) backed by persistence and migration to storage version 4.ComposerQueuedFollowUpsPanelrenders the queue with drag-and-drop reordering, per-item steer/delete/edit actions, and inline error display.lastSendErroron the item and do not dequeue it.COMPOSER_DRAFT_STORAGE_VERSIONto 4; existing persisted drafts will be migrated but the newqueuedFollowUpsByThreadIdfield will be absent until first use.📊 Macroscope summarized 8f0600a. 11 files reviewed, 6 issues evaluated, 1 issue filtered, 1 comment posted
🗂️ Filtered Issues
apps/web/src/routes/_chat.settings.tsx — 0 comments posted, 1 evaluated, 1 filtered
changedSettingLabelsarray (lines 367-386) does not include a check forsettings.followUpBehavior !== DEFAULT_UNIFIED_SETTINGS.followUpBehavior. This means when the user changes the "Follow-up behavior" setting, the "Restore defaults" button will remain disabled (or not reflect this change), and the setting won't be mentioned in the confirmation dialog when restoring defaults. [ Out of scope ]