feat(cli): wire Cursor /summarize and /clear slash builtins (#747)

* feat(cursor): wire /summarize and /clear slash builtins for remote sessions

Seed cursor builtins for web autocomplete, parse summarize/clear in
cursorRemoteLauncher (pass-through to agent -p; reject /clear with args).

Fixes tiann/hapi#738

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): isolate slash commands before message queue batching

Parse summarize/clear at enqueue time (runCursor) with pushIsolateAndClear
so waitForMessagesAndGetAsString never merges a slash with the next prompt.
Adds queue policy tests for invalid /clear + following message.

Addresses PR #747 review.

Co-authored-by: Cursor <cursoragent@cursor.com>

* 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 #747 review.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
HeavyGee
2026-06-01 12:07:57 +08:00
committed by GitHub
co-authored by Cursor
parent 02d4e93178
commit 449cf6af0a
10 changed files with 230 additions and 2 deletions
+22
View File
@@ -367,6 +367,28 @@ describe('MessageQueue2', () => {
expect(batch1?.mode.type).toBe('A');
});
it('should preserve pending messages when pushIsolated is used', async () => {
const queue = new MessageQueue2<{ type: string }>((mode) => mode.type);
queue.push('message1', { type: 'A' });
queue.push('message2', { type: 'A' });
queue.pushIsolated('isolated', { type: 'A' });
queue.push('message3', { type: 'A' });
const batch1 = await queue.waitForMessagesAndGetAsString();
expect(batch1?.message).toBe('message1\nmessage2');
expect(batch1?.isolate).toBe(false);
const batch2 = await queue.waitForMessagesAndGetAsString();
expect(batch2?.message).toBe('isolated');
expect(batch2?.isolate).toBe(true);
const batch3 = await queue.waitForMessagesAndGetAsString();
expect(batch3?.message).toBe('message3');
});
it('should isolate messages pushed with pushIsolateAndClear', async () => {
const queue = new MessageQueue2<{ type: string }>((mode) => mode.type);
+37
View File
@@ -107,6 +107,43 @@ export class MessageQueue2<T> {
logger.debug(`[MessageQueue2] pushImmediate() completed. Queue size: ${this.queue.length}`);
}
/**
* Push a message that must be processed in isolation, preserving any
* messages already queued ahead of it. The new message is never batched
* with siblings (neither the ones before it, nor any that arrive after).
* Use this when a slash command must run alone but earlier prompts must
* still be delivered in order.
*/
pushIsolated(message: string, mode: T, localId?: string): void {
if (this.closed) {
throw new Error('Cannot push to closed queue');
}
const modeHash = this.modeHasher(mode);
logger.debug(`[MessageQueue2] pushIsolated() called with mode hash: ${modeHash} - preserving ${this.queue.length} pending messages`);
this.queue.push({
message,
mode,
modeHash,
localId,
isolate: true
});
if (this.onMessageHandler) {
this.onMessageHandler(message, mode);
}
if (this.waiter) {
logger.debug(`[MessageQueue2] Notifying waiter for isolated message`);
const waiter = this.waiter;
this.waiter = null;
waiter(true);
}
logger.debug(`[MessageQueue2] pushIsolated() completed. Queue size: ${this.queue.length}`);
}
/**
* Push a message that must be processed in complete isolation.
* Clears any pending messages and ensures this message is never batched with others.