From fa1dfbdbe13e8f6e8002d8ea66f5b9fee459e829 Mon Sep 17 00:00:00 2001 From: Ammar Date: Wed, 3 Dec 2025 10:38:38 -0600 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20fix:=20batch=20multiple=20queued?= =?UTF-8?q?=20messages=20with=20proper=20display?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Show /compact rawCommand only for single compaction request - Show actual message texts when multiple messages are queued (since compaction metadata is lost when options are overwritten) - Display now matches what will actually be sent - Multiple queued messages still sent together in single turn _Generated with `mux`_ --- src/node/services/messageQueue.test.ts | 69 ++++++++++++++++++++++++-- src/node/services/messageQueue.ts | 18 ++++--- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/src/node/services/messageQueue.test.ts b/src/node/services/messageQueue.test.ts index 96774462a..bcb63b081 100644 --- a/src/node/services/messageQueue.test.ts +++ b/src/node/services/messageQueue.test.ts @@ -35,7 +35,7 @@ describe("MessageQueue", () => { expect(queue.getDisplayText()).toBe("/compact -t 3000"); }); - it("should return rawCommand even with multiple messages if last has compaction metadata", () => { + it("should show actual messages when compaction is added after normal message", () => { queue.add("First message"); const metadata: MuxFrontendMetadata = { @@ -51,8 +51,9 @@ describe("MessageQueue", () => { queue.add("Summarize this conversation...", options); - // Should use rawCommand from latest options - expect(queue.getDisplayText()).toBe("/compact"); + // When multiple messages are queued, compaction metadata is lost when sent, + // so display shows actual messages (not rawCommand) to match what will be sent + expect(queue.getDisplayText()).toBe("First message\nSummarize this conversation..."); }); it("should return joined messages when metadata type is not compaction-request", () => { @@ -116,6 +117,68 @@ describe("MessageQueue", () => { }); }); + describe("multi-message batching", () => { + it("should batch multiple follow-up messages", () => { + queue.add("First message"); + queue.add("Second message"); + queue.add("Third message"); + + expect(queue.getMessages()).toEqual(["First message", "Second message", "Third message"]); + expect(queue.getDisplayText()).toBe("First message\nSecond message\nThird message"); + }); + + it("should batch follow-up message after compaction", () => { + const metadata: MuxFrontendMetadata = { + type: "compaction-request", + rawCommand: "/compact", + parsed: {}, + }; + + queue.add("Summarize...", { + model: "claude-3-5-sonnet-20241022", + muxMetadata: metadata, + }); + queue.add("And then do this follow-up task"); + + // When a follow-up is added, compaction metadata is lost (latestOptions overwritten), + // so display shows actual messages to match what will be sent + expect(queue.getDisplayText()).toBe("Summarize...\nAnd then do this follow-up task"); + // Raw messages have the actual prompt + expect(queue.getMessages()).toEqual(["Summarize...", "And then do this follow-up task"]); + }); + + it("should produce combined message for API call", () => { + queue.add("First message", { model: "gpt-4" }); + queue.add("Second message"); + + const { message, options } = queue.produceMessage(); + + // Messages are joined with newlines + expect(message).toBe("First message\nSecond message"); + // Latest options are used + expect(options?.model).toBe("gpt-4"); + }); + + it("should batch messages with mixed images", () => { + const image1 = { url: "data:image/png;base64,abc", mediaType: "image/png" }; + const image2 = { url: "data:image/jpeg;base64,def", mediaType: "image/jpeg" }; + + queue.add("Message with image", { model: "gpt-4", imageParts: [image1] }); + queue.add("Follow-up without image"); + queue.add("Another with image", { model: "gpt-4", imageParts: [image2] }); + + expect(queue.getMessages()).toEqual([ + "Message with image", + "Follow-up without image", + "Another with image", + ]); + expect(queue.getImageParts()).toEqual([image1, image2]); + expect(queue.getDisplayText()).toBe( + "Message with image\nFollow-up without image\nAnother with image" + ); + }); + }); + describe("getImageParts", () => { it("should return accumulated images from multiple messages", () => { const image1 = { diff --git a/src/node/services/messageQueue.ts b/src/node/services/messageQueue.ts index 69f2dd0ca..21c2f6810 100644 --- a/src/node/services/messageQueue.ts +++ b/src/node/services/messageQueue.ts @@ -19,6 +19,10 @@ function isCompactionMetadata(meta: unknown): meta is CompactionMetadata { * - Message texts (accumulated) * - Latest options (model, thinking level, etc. - overwrites on each add) * - Image parts (accumulated across all messages) + * + * Display logic: + * - Single compaction request → shows rawCommand (/compact) + * - Multiple messages → shows all actual message texts (since compaction metadata is lost anyway) */ export class MessageQueue { private messages: string[] = []; @@ -63,17 +67,17 @@ export class MessageQueue { /** * Get display text for queued messages. - * Returns rawCommand if this is a compaction request, otherwise joined messages. - * Matches StreamingMessageAggregator behavior. + * - Single compaction request shows rawCommand (/compact) + * - Multiple messages or non-compaction show actual message texts */ getDisplayText(): string { - // Check if we have compaction metadata (cast from z.any() schema type) - const cmuxMetadata = this.latestOptions?.muxMetadata as unknown; - if (isCompactionMetadata(cmuxMetadata)) { - return cmuxMetadata.rawCommand; + // Only show rawCommand for single compaction request + // (compaction metadata is only preserved when no follow-up messages are added) + const muxMetadata = this.latestOptions?.muxMetadata as unknown; + if (this.messages.length === 1 && isCompactionMetadata(muxMetadata)) { + return muxMetadata.rawCommand; } - // Otherwise return joined messages return this.messages.join("\n"); }