Skip to content

feat(web): add configurable queue/steer follow-up behavior#1479

Draft
leonardoxr wants to merge 2 commits intopingdotgg:mainfrom
leonardoxr:feat/follow-up-behavior-1462
Draft

feat(web): add configurable queue/steer follow-up behavior#1479
leonardoxr wants to merge 2 commits intopingdotgg:mainfrom
leonardoxr:feat/follow-up-behavior-1462

Conversation

@leonardoxr
Copy link
Copy Markdown

@leonardoxr leonardoxr commented Mar 28, 2026

Closes #1462

Summary

Adds explicit follow-up behavior while a thread is already running.

Users can now choose a global Follow-up behavior setting:

  • Steer: send the follow-up as guidance for the active run
  • Queue: hold the follow-up and auto-send it after the current run settles

The composer also supports a one-off opposite behavior shortcut for a single message.

What changed

  • added followUpBehavior to client settings with a default of steer
  • added a settings control in chat settings for Queue vs Steer
  • kept follow-up submission available while a run is active
  • added queued follow-up persistence per thread
  • added auto-dispatch for the queue head when the thread becomes sendable again
  • added queued follow-up actions for steer, edit, delete, and reorder
  • kept steered follow-ups out of the visible chat timeline so they behave like guidance rather than a new visible turn
  • added keyboard support for sending the opposite behavior once
    • Windows/Linux: Ctrl+Shift+Enter
    • macOS: Cmd+Shift+Enter

Why

This makes follow-up delivery explicit and predictable while a run is active, which is the core problem described in #1462.

Verification

  • bun fmt
  • bun lint
  • bun typecheck
  • bun run test -- src/composerDraftStore.test.ts
  • bun run test:browser -- src/components/ChatView.browser.tsx --testNamePattern "queued|follow-up"

Media

Settings

Settings

Before

Before

After

After

Before/after comparison

Before/after comparison

Preview

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 already running: 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 new ComposerQueuedFollowUpsPanel UI 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 followUpBehavior and 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

  • Adds a followUpBehavior setting ('steer' default, 'queue' optional) to ClientSettingsSchema and exposes a selector in the chat settings UI.
  • While a thread is running, the composer's submit action either steers (sends immediately) or queues the follow-up based on the setting; a Ctrl+Shift+Enter / Cmd+Shift+Enter shortcut inverts the behavior once.
  • Queued follow-ups are managed in a new queuedFollowUpsByThreadId map in the composer draft store, with queue operations (enqueue, reorder, edit/restore, steer, delete) backed by persistence and migration to storage version 4.
  • A new ComposerQueuedFollowUpsPanel renders the queue with drag-and-drop reordering, per-item steer/delete/edit actions, and inline error display.
  • When the thread becomes ready, the first queued follow-up auto-dispatches; failures surface as lastSendError on the item and do not dequeue it.
  • Risk: Increments COMPOSER_DRAFT_STORAGE_VERSION to 4; existing persisted drafts will be migrated but the new queuedFollowUpsByThreadId field 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
  • line 367: The changedSettingLabels array (lines 367-386) does not include a check for settings.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 ]

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 28, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2b3c434f-b05b-4c56-be63-f161a8b9229c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Mar 28, 2026
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

return () => {
document.removeEventListener("pointerdown", handlePointerDown);
};
}, []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

@juliusmarminge
Copy link
Copy Markdown
Member

I want this behavior to be server side. I shouldn't need to keep my client open for the queing to work

Copy link
Copy Markdown

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

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: 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".

Comment on lines 4453 to +4455
) : 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>
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 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 👍 / 👎.

Comment on lines +2557 to +2560
enqueueQueuedFollowUp(activeThread.id, followUpSnapshot);
promptRef.current = "";
clearComposerDraftContent(activeThread.id);
setComposerHighlightedItemId(null);
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 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 👍 / 👎.

@leonardoxr
Copy link
Copy Markdown
Author

leonardoxr commented Mar 28, 2026

I want this behavior to be server side. I shouldn't need to keep my client open for the queing to work
@juliusmarminge
I will work on that! Thanks for the feedback.

);
}

export const ComposerQueuedFollowUpsPanel = memo(function ComposerQueuedFollowUpsPanel({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 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

@leonardoxr
Copy link
Copy Markdown
Author

@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"

@leonardoxr leonardoxr marked this pull request as draft March 28, 2026 03:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Add configurable follow-up behavior while a turn is running

2 participants