Skip to content

Commit 9e334db

Browse files
heavygeecursoragent
andcommitted
fix(cursor): preserve pending messages when isolating slash commands
pushIsolateAndClear() wipes the entire queue, so a normal prompt queued before /summarize or /clear would be silently dropped. Add pushIsolated() - isolation without clearing - and route Cursor slash commands through it instead. Adds queue tests covering the preserve-then-isolate path. Addresses PR tiann#747 review. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 396a030 commit 9e334db

4 files changed

Lines changed: 78 additions & 1 deletion

File tree

cli/src/cursor/cursorUserMessageQueue.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,22 @@ describe('enqueueCursorUserMessage', () => {
3232
const second = await queue.waitForMessagesAndGetAsString();
3333
expect(second?.message).toBe('next task');
3434
});
35+
36+
it('preserves a normal prompt queued before a slash command', async () => {
37+
const queue = new MessageQueue2<EnhancedMode>((m) => m.permissionMode);
38+
enqueueCursorUserMessage(queue, 'first work', mode, 'a');
39+
enqueueCursorUserMessage(queue, '/summarize', mode, 'b');
40+
enqueueCursorUserMessage(queue, 'after summarize', mode, 'c');
41+
42+
const first = await queue.waitForMessagesAndGetAsString();
43+
expect(first?.message).toBe('first work');
44+
expect(first?.isolate).toBe(false);
45+
46+
const second = await queue.waitForMessagesAndGetAsString();
47+
expect(second?.message).toBe('/summarize');
48+
expect(second?.isolate).toBe(true);
49+
50+
const third = await queue.waitForMessagesAndGetAsString();
51+
expect(third?.message).toBe('after summarize');
52+
});
3553
});

cli/src/cursor/cursorUserMessageQueue.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function enqueueCursorUserMessage(
1414
): void {
1515
const specialCommand = parseCursorSpecialCommand(formattedText);
1616
if (specialCommand.type !== null) {
17-
messageQueue.pushIsolateAndClear(formattedText.trim(), enhancedMode, localId);
17+
messageQueue.pushIsolated(formattedText.trim(), enhancedMode, localId);
1818
return;
1919
}
2020
messageQueue.push(formattedText, enhancedMode, localId);

cli/src/utils/MessageQueue2.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,28 @@ describe('MessageQueue2', () => {
367367
expect(batch1?.mode.type).toBe('A');
368368
});
369369

370+
it('should preserve pending messages when pushIsolated is used', async () => {
371+
const queue = new MessageQueue2<{ type: string }>((mode) => mode.type);
372+
373+
queue.push('message1', { type: 'A' });
374+
queue.push('message2', { type: 'A' });
375+
376+
queue.pushIsolated('isolated', { type: 'A' });
377+
378+
queue.push('message3', { type: 'A' });
379+
380+
const batch1 = await queue.waitForMessagesAndGetAsString();
381+
expect(batch1?.message).toBe('message1\nmessage2');
382+
expect(batch1?.isolate).toBe(false);
383+
384+
const batch2 = await queue.waitForMessagesAndGetAsString();
385+
expect(batch2?.message).toBe('isolated');
386+
expect(batch2?.isolate).toBe(true);
387+
388+
const batch3 = await queue.waitForMessagesAndGetAsString();
389+
expect(batch3?.message).toBe('message3');
390+
});
391+
370392
it('should isolate messages pushed with pushIsolateAndClear', async () => {
371393
const queue = new MessageQueue2<{ type: string }>((mode) => mode.type);
372394

cli/src/utils/MessageQueue2.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,43 @@ export class MessageQueue2<T> {
107107
logger.debug(`[MessageQueue2] pushImmediate() completed. Queue size: ${this.queue.length}`);
108108
}
109109

110+
/**
111+
* Push a message that must be processed in isolation, preserving any
112+
* messages already queued ahead of it. The new message is never batched
113+
* with siblings (neither the ones before it, nor any that arrive after).
114+
* Use this when a slash command must run alone but earlier prompts must
115+
* still be delivered in order.
116+
*/
117+
pushIsolated(message: string, mode: T, localId?: string): void {
118+
if (this.closed) {
119+
throw new Error('Cannot push to closed queue');
120+
}
121+
122+
const modeHash = this.modeHasher(mode);
123+
logger.debug(`[MessageQueue2] pushIsolated() called with mode hash: ${modeHash} - preserving ${this.queue.length} pending messages`);
124+
125+
this.queue.push({
126+
message,
127+
mode,
128+
modeHash,
129+
localId,
130+
isolate: true
131+
});
132+
133+
if (this.onMessageHandler) {
134+
this.onMessageHandler(message, mode);
135+
}
136+
137+
if (this.waiter) {
138+
logger.debug(`[MessageQueue2] Notifying waiter for isolated message`);
139+
const waiter = this.waiter;
140+
this.waiter = null;
141+
waiter(true);
142+
}
143+
144+
logger.debug(`[MessageQueue2] pushIsolated() completed. Queue size: ${this.queue.length}`);
145+
}
146+
110147
/**
111148
* Push a message that must be processed in complete isolation.
112149
* Clears any pending messages and ensures this message is never batched with others.

0 commit comments

Comments
 (0)