Skip to content

Commit fe82183

Browse files
committed
fix(web): drop a queued send that Stop drained mid-upload
Made with Claude Fable 5.1 in Claude Code.
1 parent 929db2d commit fe82183

3 files changed

Lines changed: 24 additions & 2 deletions

File tree

‎apps/web/src/components/ChatView.tsx‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7451,6 +7451,10 @@ export default function ChatView(props: ChatViewProps) {
74517451
return;
74527452
}
74537453
}
7454+
// Stop drains the queue. A queued send whose upload was still running at
7455+
// that moment must not start a turn afterwards; it checks this before
7456+
// dispatch and hands the message back to the composer instead.
7457+
const drainGenerationAtTake = useQueuedMessageStore.getState().drainGeneration;
74547458
// A queued send that fails goes back to the head of the queue, held. The
74557459
// messages behind it keep their order and wait; the composer is not
74567460
// touched, which also keeps a failure after navigation off the new
@@ -7495,6 +7499,15 @@ export default function ChatView(props: ChatViewProps) {
74957499
}
74967500
}
74977501

7502+
if (
7503+
queuedMessage &&
7504+
useQueuedMessageStore.getState().drainGeneration !== drainGenerationAtTake
7505+
) {
7506+
sendInFlightRef.current = false;
7507+
restoreQueuedMessagesToComposer([queuedMessage]);
7508+
return;
7509+
}
7510+
74987511
const resolvedSubmissionIntent =
74997512
submissionIntent === "background" && isLocalDraftThread ? "background" : "foreground";
75007513
if (

‎apps/web/src/queuedMessageStore.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function makeMessage(prompt: string): Omit<QueuedComposerMessage, "id"> {
2323

2424
describe("queuedMessageStore", () => {
2525
beforeEach(() => {
26-
useQueuedMessageStore.setState({ queuesByThreadKey: {} });
26+
useQueuedMessageStore.setState({ queuesByThreadKey: {}, drainGeneration: 0 });
2727
});
2828

2929
it("keeps messages in submission order per thread", () => {
@@ -93,7 +93,9 @@ describe("queuedMessageStore", () => {
9393
enqueue("thread-b", makeMessage("other"));
9494

9595
expect(drain("thread-a").map((message) => message.prompt)).toEqual(["first", "second"]);
96+
expect(useQueuedMessageStore.getState().drainGeneration).toBe(1);
9697
expect(drain("thread-a")).toEqual([]);
98+
expect(useQueuedMessageStore.getState().drainGeneration).toBe(1);
9799
expect(useQueuedMessageStore.getState().queuesByThreadKey["thread-b"]).toHaveLength(1);
98100
});
99101
});

‎apps/web/src/queuedMessageStore.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export interface QueuedComposerMessage {
3737

3838
interface QueuedMessageStoreState {
3939
queuesByThreadKey: Record<string, QueuedComposerMessage[]>;
40+
/**
41+
* Bumped by `drain`. A send that took a message before a drain and finishes
42+
* its upload after it compares this to the value it captured and gives up,
43+
* so Stop cannot be followed by a queued message starting a new turn.
44+
*/
45+
drainGeneration: number;
4046
enqueue: (threadKey: string, message: Omit<QueuedComposerMessage, "id">) => QueuedComposerMessage;
4147
/**
4248
* Removes one message and returns it, or null when another caller already
@@ -64,6 +70,7 @@ const EMPTY_QUEUE: QueuedComposerMessage[] = [];
6470
/** In-memory only: a queued message is a live intent, not a draft worth persisting. */
6571
export const useQueuedMessageStore = create<QueuedMessageStoreState>()((set, get) => ({
6672
queuesByThreadKey: {},
73+
drainGeneration: 0,
6774
enqueue: (threadKey, message) => {
6875
const entry: QueuedComposerMessage = { ...message, id: randomUUID() };
6976
set((state) => ({
@@ -139,7 +146,7 @@ export const useQueuedMessageStore = create<QueuedMessageStoreState>()((set, get
139146
set((state) => {
140147
const queuesByThreadKey = { ...state.queuesByThreadKey };
141148
delete queuesByThreadKey[threadKey];
142-
return { queuesByThreadKey };
149+
return { queuesByThreadKey, drainGeneration: state.drainGeneration + 1 };
143150
});
144151
return queue;
145152
},

0 commit comments

Comments
 (0)