Skip to content

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

Open
leonardoxr wants to merge 19 commits intopingdotgg:mainfrom
leonardoxr:feat/follow-up-behavior-1462
Open

feat(web): add configurable queue/steer follow-up behavior#1479
leonardoxr wants to merge 19 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
Adds new persisted queued-follow-up state plus an always-on reactor that can auto-dispatch turns, touching core orchestration/projection paths and attachment validation; regressions could lead to unexpected sends or orphaned files.

Overview
Adds first-class queued follow-ups to orchestration: new thread.queued-follow-up.* commands/events are projected into a new persisted queue table and surfaced in snapshots as thread.queuedFollowUps.

Introduces a new QueuedFollowUpReactor that watches thread events/read-model state and automatically dispatches the queue head when the thread becomes sendable (including applying queued runtime/interaction modes and persisting send failures).

Extends attachment handling to queued follow-ups: the projection pipeline now prunes queued-follow-up attachment files on queue mutations/reverts, and wsServer normalizes queued follow-up attachments (persist data URLs, and validate persisted attachment IDs are thread-scoped). Integration/unit tests were updated/added to cover restart replay, snapshot hydration, queue projections, and attachment pruning/validation.

Written by Cursor Bugbot for commit a2754fa. This will update automatically on new commits. Configure here.

Note

Add configurable queue/steer follow-up behavior for chat turns

  • Introduces a FollowUpBehavior setting ('queue' or 'steer', default 'steer') in client settings and the General Settings panel, controlling what happens when a user submits a follow-up while a turn is running.
  • Adds a full queued follow-up lifecycle: users can enqueue, edit, reorder, delete, and steer queued follow-ups via a new ComposerQueuedFollowUpsPanel UI; a platform-aware keyboard shortcut (Cmd/Ctrl+Shift+Enter) inverts the configured behavior inline.
  • Implements a server-side QueuedFollowUpReactor background worker that auto-dispatches the head queued follow-up when the thread is ready, handling send errors and blocking on failure.
  • Persists queued follow-ups in a new projection_thread_queued_follow_ups table (migration 019) with full attachment, terminal context, and model selection storage; adds a queuedFollowUps array to the orchestration read model and snapshot.
  • Adds new orchestration commands and events for enqueue, update, remove, reorder, send-failed, and send-error-cleared follow-up lifecycle transitions in the decider and projector.
  • Risk: increments COMPOSER_DRAFT_STORAGE_VERSION to 4, which will invalidate previously persisted composer draft state.

Macroscope summarized a2754fa.

@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: 7eec4961-e8c1-44c0-9070-c6b8c3528e95

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

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

@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
@leonardoxr
Copy link
Copy Markdown
Author

Follow-up update on this PR after the review and manual testing:

I moved queued follow-ups out of the client draft store and into server-owned orchestration state.

What changed:

  • Added durable queued follow-up contracts to the orchestration model, including prompt, persisted attachments, terminal context snapshots, model/runtime/interaction settings, and per-item send error state.
  • Added projection storage + migration for queued follow-ups, and included queuedFollowUps in thread snapshots so the web renders queue state from the server read model instead of local draft state.
  • Added a dedicated queued follow-up reactor on the server. It now owns head-only auto-dispatch, startup recovery after restart, and blocked-state gating.
  • Queued image attachments are normalized into persisted server attachments at enqueue time, so queued items survive reconnect/browser close instead of depending on client-local blob/data URLs.
  • The client queue panel now round-trips reorder/delete/queue/steer actions through orchestration commands, while local draft state only keeps composer/edit metadata.

I also fixed two regressions uncovered while testing:

  • Queue dispatch race: multiple queued items could drain back-to-back before the previous queued send visibly settled. The reactor now keeps a per-thread dispatch lock until that queued send settles.
  • Steer visibility: the browser tests were enforcing hidden steer messages, which matched the old hidden-message path in the implementation. Steered follow-ups now show up as normal visible user messages after the interrupt completes, and the browser assertions were updated accordingly.

Coverage added/updated for:

  • queue survives orchestration restart and replays later
  • queued image attachments are persisted server-side
  • reactor blocks on pending approval / pending user-input
  • queued-item steer interrupts before send
  • visible steer behavior from the composer and opposite-submit shortcut

Validation I ran before pushing this update:

  • bun fmt
  • bun lint
  • bun typecheck
  • bun run test -- src/orchestration/Layers/QueuedFollowUpReactor.test.ts --testNamePattern "pending approval|pending user-input"
  • bun run test -- src/wsServer.test.ts --testNamePattern "normalizes queued follow-up image attachments into persisted server attachments"
  • bun run test -- integration/orchestrationEngine.integration.test.ts --testNamePattern "replays queued follow-ups after orchestration restarts"
  • bun run test -- src/components/ChatView.logic.test.ts src/composerDraftStore.test.ts src/store.test.ts
  • bun run test:browser -- src/components/ChatView.browser.tsx --testNamePattern "queued|follow-up|steer|Ctrl\\+Shift\\+Enter|Cmd\\+Shift\\+Enter"

@leonardoxr
Copy link
Copy Markdown
Author

Addressed the remaining review items in this branch:

  • Moved the queued follow-ups panel hooks above the empty-state early return so it no longer risks a Rules of Hooks crash when the queue transitions between empty and non-empty.
  • Kept the interrupt control available while drafting a follow-up during an active run.
  • Guarded queued attachment hydration so a missing previewUrl now fails explicitly instead of fetch("")ing the current page.
  • Switched the queued-attachment ws test back to makeTempDir(...) and added retrying cleanup to avoid the Windows EPERM teardown issue.
  • Updated the queued projector handlers to decode their payloads through decodeForEvent(...) for consistency with the rest of the projector error-handling path.
  • Added/updated browser coverage for the stop button staying available while typing a running follow-up.

Validation rerun after these fixes:

  • bun fmt
  • bun lint
  • bun typecheck
  • bun run test -- src/wsServer.test.ts src/orchestration/projector.test.ts
  • bun run test:browser -- src/components/ChatView.browser.tsx --testNamePattern "shows a pointer cursor for the running stop button|keeps the running stop button available while drafting a follow-up|queued|follow-up|steer|Ctrl\\+Shift\\+Enter|Cmd\\+Shift\\+Enter"
  • bun run test -- src/components/ChatView.logic.test.ts src/composerDraftStore.test.ts src/store.test.ts

@leonardoxr leonardoxr marked this pull request as ready for review March 28, 2026 21:21
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: 775ee4286c

ℹ️ 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".

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: cace8fccce

ℹ️ 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".

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: 06d68cdfaf

ℹ️ 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".

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: 46f95dac9d

ℹ️ 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".

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: d92e28bc8e

ℹ️ 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".

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: f049043dd4

ℹ️ 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".

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: 8162f8644e

ℹ️ 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".

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: 711f86d0eb

ℹ️ 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".

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: 6f39aeb640

ℹ️ 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".

}),
);

if (Exit.isFailure(removeExit)) {
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.

🟡 Medium Layers/QueuedFollowUpReactor.ts:203

When thread.queued-follow-up.remove dispatch fails but the follow-up send-failed dispatch succeeds, the in-memory blockedQueuedFollowUpIdsByThreadId is only set when the send-failed dispatch itself fails (lines 98-107, 185, 224). If the send-failed succeeds but the remove failed, the follow-up remains in the queue with lastSendError set, but processThread will still attempt to re-dispatch on the next event because canDispatchQueuedFollowUp checks the in-memory blockedQueuedFollowUpIdsByThreadId first (line 61-70) and only falls back to checking queuedHead.lastSendError if the in-memory block is absent. Since the send-failed event may not yet be projected when processThread re-runs, queuedHead.lastSendError could still be null, allowing a duplicate send.

🤖 Copy this AI Prompt to have your agent fix this:
In file apps/server/src/orchestration/Layers/QueuedFollowUpReactor.ts around line 203:

When `thread.queued-follow-up.remove` dispatch fails but the follow-up `send-failed` dispatch succeeds, the in-memory `blockedQueuedFollowUpIdsByThreadId` is only set when the `send-failed` dispatch itself fails (lines 98-107, 185, 224). If the `send-failed` succeeds but the `remove` failed, the follow-up remains in the queue with `lastSendError` set, but `processThread` will still attempt to re-dispatch on the next event because `canDispatchQueuedFollowUp` checks the in-memory `blockedQueuedFollowUpIdsByThreadId` first (line 61-70) and only falls back to checking `queuedHead.lastSendError` if the in-memory block is absent. Since the `send-failed` event may not yet be projected when `processThread` re-runs, `queuedHead.lastSendError` could still be `null`, allowing a duplicate send.

Evidence trail:
apps/server/src/orchestration/Layers/QueuedFollowUpReactor.ts lines 61-70 (blockedQueuedFollowUpIdsByThreadId check), lines 79-87 (canDispatchQueuedFollowUp call with queuedHeadHasError), lines 185-226 (removeExit failure handling where blockedQueuedFollowUpIdsByThreadId is only set in catchCause at lines 214-223), lines 248-256 (event stream triggers processThread). packages/shared/src/orchestration.ts lines 232-252 (canDispatchQueuedFollowUp function returns false when queuedHeadHasError is true). Commit: REVIEWED_COMMIT

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.

(entry) => entry.id === threadId && entry.deletedAt === null,
);
return thread ? collectPersistedAttachmentIdsForThread(thread) : new Set<string>();
});
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.

Full DB snapshot loaded for single-thread attachment validation

Medium Severity

listThreadPersistedAttachmentIds calls projectionReadModelQuery.getSnapshot(), which executes a full transactional read of every projection table (projects, threads, messages, activities, sessions, queued follow-ups, checkpoints, turns, state) just to collect image attachment IDs for a single thread. The in-memory read model via orchestrationEngine.getReadModel() — already resolved in the same scope on line 337 — provides the same data without any database I/O. This runs on the command dispatch hot path for every thread.turn.start, thread.queued-follow-up.enqueue, or thread.queued-follow-up.update that references a previously persisted attachment.

Fix in Cursor Fix in Web

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