Replies: 27 comments 1 reply
|
+1 for this, for sure for "Steer". This is very usefull function in Codex. |
|
@MatejSkarka You can already steer by just adding messages, so basically only queueing is missing with clear UI, from my understanding |
|
yes it's all steer now. we'll add queinig! |
|
+1 Queue is very important |
|
This is a big one and super important for me |
PRD: Steer & Queue Interaction Model1. ObjectiveTo transition the interaction model from a synchronous "wait-and-reply" system to an asynchronous "task-pipeline" system. This allows users to stack instructions while a run is active and selectively "promote" urgent guidance to the active run (Steering). 2. Current State Analysis2.1 Existing Flow (Synchronous)The current implementation follows a strict synchronous message flow: Key Characteristics:
2.2 Current Schemas (Relevant Excerpts)// OrchestrationThread - current structure
const OrchestrationThread = Schema.Struct({
id: Schema.String,
projectId: Schema.String,
title: Schema.String.pipe(Schema.optional),
model: Schema.String.pipe(Schema.optional),
runtimeMode: RuntimeMode,
interactionMode: ProviderInteractionMode,
branch: Schema.String.pipe(Schema.optional),
worktreePath: Schema.String.pipe(Schema.optional),
latestTurn: OrchestrationLatestTurn.pipe(Schema.optional),
messages: Schema.Array(OrchestrationMessage),
proposedPlans: Schema.Array(OrchestrationProposedPlan),
activities: Schema.Array(OrchestrationThreadActivity),
checkpoints: Schema.Array(OrchestrationCheckpointSummary),
session: OrchestrationSession.pipe(Schema.optional),
// ... timestamps
});
// OrchestrationSession - tracks active provider state
const OrchestrationSession = Schema.Struct({
threadId: Schema.String,
status: OrchestrationSessionStatus, // "idle" | "running" | "error"
providerName: Schema.String.pipe(Schema.optional),
runtimeMode: RuntimeMode,
activeTurnId: Schema.String.pipe(Schema.optional),
lastError: Schema.String.pipe(Schema.optional),
updatedAt: Schema.String,
});
// OrchestrationLatestTurn - turn lifecycle state
const OrchestrationLatestTurn = Schema.Struct({
turnId: Schema.String,
state: OrchestrationLatestTurnState, // "requested" | "running" | "completed"
requestedAt: Schema.String,
startedAt: Schema.String.pipe(Schema.optional),
completedAt: Schema.String.pipe(Schema.optional),
assistantMessageId: Schema.String.pipe(Schema.optional),
});2.3 Existing Commands
3. User Problems
4. Proposed Features & Logic4.1 The Follow-Up Queue
4.2 The "Steer" Promotion
4.3 Queue Operations
4.4 Settings Integration// New settings schema
const FollowUpModeSettings = Schema.Struct({
followUpMode: Schema.Literal("queue", "steer"), // Default: "queue"
showFollowUpModeInComposer: Schema.Boolean, // Default: true
});
5. UI/UX DesignThe UI sits directly above the Composer to indicate "input-ready" but "pending-execution." 5.1 UI Layout Example5.2 Composer Footer States5.3 Component Breakdown
6. Technical Requirements6.1 Schema Changes (packages/contracts/src/orchestration.ts)// Add to OrchestrationThread
const OrchestrationQueuedFollowUp = Schema.Struct({
id: Schema.String,
message: Schema.Struct({
text: Schema.String,
attachments: Schema.Array(ChatAttachment).pipe(Schema.optional),
}),
queuedAt: Schema.String,
mode: Schema.Literal("queue", "steer"),
});
// Updated OrchestrationThread
const OrchestrationThread = Schema.Struct({
// ... existing fields ...
queuedFollowUps: Schema.Array(OrchestrationQueuedFollowUp), // NEW
});
// New Commands
const ThreadFollowUpQueueCommand = Schema.Struct({
type: Schema.Literal("thread.follow-up.queue"),
commandId: Schema.String,
threadId: Schema.String,
message: Schema.Struct({
text: Schema.String,
attachments: Schema.Array(ChatAttachment).pipe(Schema.optional),
}),
mode: Schema.Literal("queue", "steer").pipe(Schema.optional),
createdAt: Schema.String,
});
const ThreadFollowUpDequeueCommand = Schema.Struct({
type: Schema.Literal("thread.follow-up.dequeue"),
commandId: Schema.String,
threadId: Schema.String,
followUpId: Schema.String.pipe(Schema.optional), // If omitted, dequeues first (FIFO)
createdAt: Schema.String,
});
const ThreadTurnSteerCommand = Schema.Struct({
type: Schema.Literal("thread.turn.steer"),
commandId: Schema.String,
threadId: Schema.String,
turnId: Schema.String,
message: Schema.Struct({
text: Schema.String,
attachments: Schema.Array(ChatAttachment).pipe(Schema.optional),
}),
createdAt: Schema.String,
});
// New Events
const ThreadFollowUpQueuedPayload = Schema.Struct({
threadId: Schema.String,
followUp: OrchestrationQueuedFollowUp,
});
const ThreadFollowUpDequeuedPayload = Schema.Struct({
threadId: Schema.String,
followUpId: Schema.String,
});
const ThreadTurnSteeredPayload = Schema.Struct({
threadId: Schema.String,
turnId: Schema.String,
followUpId: Schema.String.pipe(Schema.optional),
messageText: Schema.String,
});6.2 Queue Limits (Constants)// packages/contracts/src/orchestration.ts
export const MAX_QUEUED_FOLLOW_UPS = 10;6.3 Server-Side Implementation (apps/server/src/orchestration/decider.ts)New command handlers in
6.4 Reactor Logic (apps/server/src/orchestration/Layers/ProviderCommandReactor.ts)On // After turn completion, check for queued follow-ups
if (readModel.threads.find((t) => t.id === threadId)?.queuedFollowUps.length > 0) {
// Dequeue first item
const nextFollowUp = thread.queuedFollowUps[0];
// Dispatch ThreadTurnStartCommand with the queued message
await dispatchCommand({
type: "thread.turn-start",
threadId,
message: {
messageId: generateId(),
role: "user",
text: nextFollowUp.message.text,
attachments: nextFollowUp.message.attachments,
},
// ... other fields
});
}6.5 Frontend State Management (apps/web/src)// apps/web/src/state/threadStore.ts
interface ThreadState {
// ... existing fields
queuedFollowUps: OrchestrationQueuedFollowUp[];
}
// Selectors
const selectQueuedFollowUps = (threadId: string) => (state) =>
state.threads[threadId]?.queuedFollowUps ?? [];
const selectQueueCount = (threadId: string) => (state) =>
state.threads[threadId]?.queuedFollowUps.length ?? 0;
const selectCanQueue = (threadId: string) => (state) =>
(state.threads[threadId]?.queuedFollowUps.length ?? 0) < 10;6.6 WebSocket IntegrationQueue operations use the existing // Client dispatches
ws.dispatchCommand({
type: "thread.follow-up.queue",
threadId: "thread-123",
message: { text: "Also fix the tests" },
});
// Server pushes domain events
ws.push("orchestration.domainEvent", {
type: "thread.follow-up-queued",
payload: { threadId: "thread-123", followUp: {...} },
});7. Persistence & Recovery7.1 Event Sourcing
7.2 Thread Snapshot
8. Acceptance Criteria8.1 Core Functionality
8.2 State Synchronization
8.3 Keyboard Shortcuts
8.4 Error Handling
|
|
Hi @juliusmarminge @maria-rcks , I have written a short PRD for this task and shared my thoughts on how the feature could be implemented. Could you please review it and let me know what you think? If the proposal looks good to you, I would be happy to take ownership of the implementation. Please feel free to assign this task to me. |
|
I'm not a maintainer, but I did implement and validate a local version of this flow, and found a few semantics that may be worth making explicit in the PRD:
I only caught this in real-browser validation, so I’m attaching a couple of short GIFs in case they help make the interaction clearer: Queued item stays pending, then appears only after drain. Queue default + immediate "Send now" override in the same run. Not trying to push a different architecture, just sharing a few implementation details that turned out to matter a lot for the UI. |
|
+1 for queue support please... |
|
+1! Is this WIP? |
|
"/btw" would also be sickkk |
|
I ended up making my own fork here: https://github.com/cafeai/cafe-code Not trying to compete with T3 Code at all — I just wanted something more minimal for my own use, and wanted to be able to fix the bugs I was hitting quickly. Really appreciate all the work the T3 team has put into the project so far.
|
|
Any plans for implementing a queue? |
|
not being able to queue is currently preventing me from moving to t3 code from codex. also one big thing to me for queuing is how it behaves with remote hosts:
|
|
Hello Julius. I’m working on a unified implementation for queued follow-ups so the behavior works consistently across providers. I’ve tried to reuse the existing orchestration/projection paths and keep the feature contained, though the final diff will still be non-trivial because it includes provider coverage and tests. I also reused existing UI elements where possible and kept the layout constrained to the composer width for mobile/resizing. I think the shape is in a good place now; I’m doing a bit more dogfooding across providers before opening the PR. |
|
#2829 has server managed queueing (so it works on remote hosts even if u close the client connnection etc) |
|
Thanks, that makes sense. I took a closer look at #2829 and noticed it already models the queue/steer UX contract: normal send can steer an active run, Cmd/Ctrl+Enter can queue, queued work is server-managed, and queued items can be reordered/promoted to steer. Would it be useful if I opened a smaller current-main PR that backports that #2829 queue/steer behavior without depending on the full Orchestrator V2 branch? I would align it with #2829 rather than introduce a competing UX direction. If you prefer waiting for #2829 instead, I can hold off and switch to dogfooding/reviewing that path. |
|
hi! any update on this pr? |
|
Yes, I think this is an essential feature too. Any updates? |
|
Definitely something I'd love to see. Sometimes I like to queue multiple follow ups for longer prompts if I know I'm going to be AFK and there is just no way of doing that currently. Would be greatly appreciated :) |
|
This is the feature that's blocking me from switchign to t3 from codex |
Mobile: allow users to review and edit queued messagesOn the T3 Code mobile view, after I queue a follow-up while the agent is working, I can only see:
I cannot review what I queued or correct it before it is automatically sent. Could this queue notice be tappable and open a compact queue panel or bottom sheet? Suggested actions:
This is especially important on mobile, where typing errors, voice-input mistakes, and accidental submissions are more common. A queued instruction can significantly change a long-running coding task, so users should have a chance to verify it before automatic dispatch. Minimum useful version: tapping the queued-message notice shows the pending message and provides Edit and Delete actions. This is a mobile UX follow-up to the Queue/Steer work discussed in this issue. |
|
Definitely need this feature! |
|
Adding a native Android use case in support of this request: In the official T3 Code Android app on a Google Pixel 7a, a follow-up prompt sent while the model is working is currently queued. I would like an explicit way to Steer the active run immediately while keeping Queue available as the alternative. This is especially useful when I notice that the model has misunderstood a detail or is heading in the wrong direction: I want to correct it without stopping the turn, waiting for completion, or starting over. A clearly visible Steer/Queue choice in the native composer would make the behavior predictable. Thanks for working on this. I would be happy to test the Android implementation. |
|
I would strongly prefer this to follow the native Codex Queue/Steer UX as closely as possible, with cross-platform queue state treated as a first-class requirement. Desired interaction modelWhile the agent is already working, every new message should have two explicit delivery choices:
Settings should let the user choose which behavior is the default. My preferred default is Queue. The normal Send action should use that default, while a modifier key, alternate keyboard shortcut, button hold/long-press, or send-menu action should perform the opposite behavior for that one message. This should work consistently with mouse, keyboard, touch, and mobile interactions rather than being limited to a desktop-only hover control. Match the Codex queue UIWhen messages are queued, show every pending message clearly in the conversation/composer area, in execution order. For each queued message, provide an obvious Send now action that immediately promotes that specific message from queued to Steer, matching the behavior of sending it immediately in Codex. The user should always be able to tell:
Queued messages should not disappear into an opaque count or work log. Cross-platform synchronizationThe queue must belong to the thread/session on the T3 server, not to the client where the message was composed. For example, I should be able to:
Queue mutations should be authoritative and ordered so simultaneous clients cannot duplicate, lose, or reorder messages. Closing or refreshing the originating client must not remove queued messages or prevent them from executing. Additional acceptance criteria
The existing technical direction in this issue is strong; the key product requirement for me is that the result feels like the Codex implementation rather than merely exposing two backend commands. |
|
Status check for anyone landing here from search: as of Mobile's queue-while-busy behavior was removed. PR #6543 ("fix(mobile): steer active turns by default", merged 2026-08-14) dropped the busy guard from the outbox delivery resolver: - return input.environmentConnected && !input.threadBusy ? "send" : "wait";
+ return input.environmentConnected ? "send" : "wait";It also deleted the post-await re-check that deferred delivery when the thread had gone busy. That directly answers @fionn77's and @sprintthunder-dotcom's reports above — the Android behavior they described as "queued" was real, and it was removed two weeks later in favor of desktop parity (#7234). The net effect is that Queue now exists on zero platforms, which is the opposite of the "cross-platform queue state as a first-class requirement" @ElliotDrel asked for. Desktop has only ever steered. if (input.phase === "running") {
// Steering adds a user message to the current running turn without
// necessarily changing any of the turn timestamps.And the surrounding contract surface is empty: no The prompt stash isn't a substitute. It's genuinely useful and it's what I'm using today, but it's manual hold-and-resend from a drawer — nothing drains it when the turn settles, which is the whole point of Queue. It's also Why steer-only hurts in practice. Mid-turn the only choices are interrupt or steer, and neither is right for a strictly sequential follow-up ("then run the tests") — steering makes the agent context-switch mid-task instead of finishing. Steering also still has live sharp edges: #2573 (opencode: steering breaks session tracking and Stop stops working afterwards). The acceptance criteria in the original post still read exactly right, and #6885 is the same ask with 11 upvotes. Is this on the roadmap, or is steer-only the intended long-term model? If it's the latter, saying so would at least stop people rediscovering the mobile regression and filing it as a bug. |
|
can we queue messages yet? |



Uh oh!
There was an error while loading. Please reload this page.
Right now T3 Code appears to only model
defaultandplaninteraction modes. I’d like to request support forSteerandQueuestyle follow-up behavior while a run is already active.Why
When the agent is already working, users often want two different behaviors:
Steer: inject immediate guidance into the active runQueue: save the follow-up so it runs after the current work settlesThis is a meaningful UX distinction and avoids forcing users into interrupt-or-wait behavior only.
Settings ask
It would be even better if this can be configured in Settings, for example:
Steer/QueueexplicitlySuggested acceptance criteria
Relevant code context
packages/contracts/src/orchestration.tsapps/web/src/components/ChatView.tsxapps/server/src/orchestration/Layers/ProviderCommandReactor.tsReference implementation ideas
QueuevsSteerwhile a run is active.All reactions