Skip to content

Commit d2fea88

Browse files
authored
fix(sessions): disable steer for cloud tasks to stop spurious interruptions (#2991)
1 parent 8f2c339 commit d2fea88

4 files changed

Lines changed: 57 additions & 13 deletions

File tree

packages/core/src/sessions/sessionService.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1902,13 +1902,22 @@ export class SessionService {
19021902
}
19031903

19041904
// Steer: the user sent a message mid-turn and asked to fold it into the
1905-
// running turn rather than queue it. Native (Claude) injects at the next
1906-
// tool boundary; everything else interrupts the turn and resends below as a
1907-
// fresh prompt. Compaction always falls through to the queue.
1908-
if (options?.steer && session.isPromptPending && !session.isCompacting) {
1909-
const supportsNativeSteer =
1910-
!session.isCloud && session.adapter === "claude";
1911-
if (supportsNativeSteer) {
1905+
// running turn rather than queue it. Native (Claude, local) injects at the
1906+
// next tool boundary; local Codex interrupts the turn and resends below as
1907+
// a fresh prompt.
1908+
//
1909+
// Cloud has no real mid-turn steer: the backend only delivers user messages
1910+
// between turns, so a cloud "steer" would cancel the running turn for no
1911+
// gain (the message lands next turn either way) while surfacing a jarring
1912+
// interruption. Until the backend supports true steering, cloud steer falls
1913+
// through to the queue like a normal message. Compaction also falls through.
1914+
if (
1915+
options?.steer &&
1916+
!session.isCloud &&
1917+
session.isPromptPending &&
1918+
!session.isCompacting
1919+
) {
1920+
if (session.adapter === "claude") {
19121921
return this.sendSteerPrompt(session, prompt);
19131922
}
19141923
await this.cancelPrompt(taskId);

packages/ui/src/features/sessions/components/QueuedMessagesDock.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ export function QueuedMessagesDock({ taskId }: QueuedMessagesDockProps) {
2828
const sessionService = useService<SessionService>(SESSION_SERVICE);
2929
const supportsNativeSteer = useSupportsNativeSteer(taskId);
3030
const returnToEditor = useReturnQueuedMessageToEditor(taskId);
31+
const session = useSessionForTask(taskId);
3132
// Steer can't inject mid-compaction, so it would be a silent no-op; hide it.
32-
const isCompacting = useSessionForTask(taskId)?.isCompacting ?? false;
33+
// Cloud has no real mid-turn steer either (it would just interrupt the turn),
34+
// so hide it there too — the message stays queued and lands next turn.
35+
const canSteer =
36+
!(session?.isCompacting ?? false) && !(session?.isCloud ?? false);
3337

3438
if (queued.length === 0) return null;
3539

@@ -41,9 +45,8 @@ export function QueuedMessagesDock({ taskId }: QueuedMessagesDockProps) {
4145
message={message}
4246
supportsNativeSteer={supportsNativeSteer}
4347
onSteer={
44-
isCompacting
45-
? undefined
46-
: () => {
48+
canSteer
49+
? () => {
4750
void sessionService
4851
.steerQueuedMessage(taskId, message.id)
4952
.catch(() => {
@@ -52,6 +55,7 @@ export function QueuedMessagesDock({ taskId }: QueuedMessagesDockProps) {
5255
);
5356
});
5457
}
58+
: undefined
5559
}
5660
onReturnToEditor={() => returnToEditor(message)}
5761
onRemove={() =>

packages/ui/src/features/sessions/components/SessionView.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -650,11 +650,13 @@ export function SessionView({
650650
) : null
651651
}
652652
messagingModeToggle={
653-
taskId ? (
653+
taskId && !isCloudRun ? (
654654
<SteerQueueToggle taskId={taskId} />
655655
) : undefined
656656
}
657-
onToggleMessagingMode={toggleMessagingMode}
657+
onToggleMessagingMode={
658+
isCloudRun ? undefined : toggleMessagingMode
659+
}
658660
onBeforeSubmit={handleBeforeSubmit}
659661
onSubmit={handleSubmit}
660662
onBashCommand={onBashCommand}

packages/ui/src/features/sessions/sessionServiceHost.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3644,6 +3644,35 @@ describe("SessionService", () => {
36443644
expect(mockTrpcCloudTask.sendCommand.mutate).not.toHaveBeenCalled();
36453645
});
36463646

3647+
it("queues a cloud steer instead of interrupting the running turn", async () => {
3648+
// Regression: cloud has no native mid-turn steer, so steering used to
3649+
// fall back to cancel-then-resend — which surfaced as a jarring user
3650+
// interruption. Cloud steer must now queue like a normal message and
3651+
// never cancel the running turn.
3652+
const service = getSessionService();
3653+
mockSessionStoreSetters.getSessionByTaskId.mockReturnValue(
3654+
createMockSession({
3655+
isCloud: true,
3656+
cloudStatus: "in_progress",
3657+
status: "connected",
3658+
isPromptPending: true,
3659+
}),
3660+
);
3661+
3662+
const prompt: ContentBlock[] = [{ type: "text", text: "steer me" }];
3663+
const result = await service.sendPrompt("task-123", prompt, {
3664+
steer: true,
3665+
});
3666+
3667+
expect(result.stopReason).toBe("queued");
3668+
expect(mockSessionStoreSetters.enqueueMessage).toHaveBeenCalledWith(
3669+
"task-123",
3670+
"steer me",
3671+
prompt,
3672+
);
3673+
expect(mockTrpcCloudTask.sendCommand.mutate).not.toHaveBeenCalled();
3674+
});
3675+
36473676
it("kicks an SSE retry when queueing on a disconnected cloud session", async () => {
36483677
const service = getSessionService();
36493678
mockSessionStoreSetters.getSessionByTaskId.mockReturnValue(

0 commit comments

Comments
 (0)