Skip to content

Commit f3b6e4f

Browse files
committed
🤖 fix: batch multiple queued messages with proper display
- Track per-message display text in MessageQueue - Compaction commands show their rawCommand, normal messages show their text - All queued messages are shown newline-separated - Follow-up messages during compaction now visible in queue _Generated with `mux`_
1 parent cdeffed commit f3b6e4f

File tree

2 files changed

+77
-13
lines changed

2 files changed

+77
-13
lines changed

src/node/services/messageQueue.test.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe("MessageQueue", () => {
3535
expect(queue.getDisplayText()).toBe("/compact -t 3000");
3636
});
3737

38-
it("should return rawCommand even with multiple messages if last has compaction metadata", () => {
38+
it("should show all messages when compaction is added after normal message", () => {
3939
queue.add("First message");
4040

4141
const metadata: MuxFrontendMetadata = {
@@ -51,8 +51,8 @@ describe("MessageQueue", () => {
5151

5252
queue.add("Summarize this conversation...", options);
5353

54-
// Should use rawCommand from latest options
55-
expect(queue.getDisplayText()).toBe("/compact");
54+
// Should show all messages: first message + compaction rawCommand
55+
expect(queue.getDisplayText()).toBe("First message\n/compact");
5656
});
5757

5858
it("should return joined messages when metadata type is not compaction-request", () => {
@@ -116,6 +116,67 @@ describe("MessageQueue", () => {
116116
});
117117
});
118118

119+
describe("multi-message batching", () => {
120+
it("should batch multiple follow-up messages", () => {
121+
queue.add("First message");
122+
queue.add("Second message");
123+
queue.add("Third message");
124+
125+
expect(queue.getMessages()).toEqual(["First message", "Second message", "Third message"]);
126+
expect(queue.getDisplayText()).toBe("First message\nSecond message\nThird message");
127+
});
128+
129+
it("should batch follow-up message after compaction", () => {
130+
const metadata: MuxFrontendMetadata = {
131+
type: "compaction-request",
132+
rawCommand: "/compact",
133+
parsed: {},
134+
};
135+
136+
queue.add("Summarize...", {
137+
model: "claude-3-5-sonnet-20241022",
138+
muxMetadata: metadata,
139+
});
140+
queue.add("And then do this follow-up task");
141+
142+
// Display shows slash command for compaction, then the follow-up
143+
expect(queue.getDisplayText()).toBe("/compact\nAnd then do this follow-up task");
144+
// Raw messages have the actual prompt
145+
expect(queue.getMessages()).toEqual(["Summarize...", "And then do this follow-up task"]);
146+
});
147+
148+
it("should produce combined message for API call", () => {
149+
queue.add("First message", { model: "gpt-4" });
150+
queue.add("Second message");
151+
152+
const { message, options } = queue.produceMessage();
153+
154+
// Messages are joined with newlines
155+
expect(message).toBe("First message\nSecond message");
156+
// Latest options are used
157+
expect(options?.model).toBe("gpt-4");
158+
});
159+
160+
it("should batch messages with mixed images", () => {
161+
const image1 = { url: "data:image/png;base64,abc", mediaType: "image/png" };
162+
const image2 = { url: "data:image/jpeg;base64,def", mediaType: "image/jpeg" };
163+
164+
queue.add("Message with image", { model: "gpt-4", imageParts: [image1] });
165+
queue.add("Follow-up without image");
166+
queue.add("Another with image", { model: "gpt-4", imageParts: [image2] });
167+
168+
expect(queue.getMessages()).toEqual([
169+
"Message with image",
170+
"Follow-up without image",
171+
"Another with image",
172+
]);
173+
expect(queue.getImageParts()).toEqual([image1, image2]);
174+
expect(queue.getDisplayText()).toBe(
175+
"Message with image\nFollow-up without image\nAnother with image"
176+
);
177+
});
178+
});
179+
119180
describe("getImageParts", () => {
120181
it("should return accumulated images from multiple messages", () => {
121182
const image1 = {

src/node/services/messageQueue.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ function isCompactionMetadata(meta: unknown): meta is CompactionMetadata {
1717
*
1818
* Stores:
1919
* - Message texts (accumulated)
20+
* - Display texts (per message, handles slash commands like /compact)
2021
* - Latest options (model, thinking level, etc. - overwrites on each add)
2122
* - Image parts (accumulated across all messages)
2223
*/
2324
export class MessageQueue {
2425
private messages: string[] = [];
26+
private displayTexts: string[] = []; // Per-message display text
2527
private latestOptions?: SendMessageOptions;
2628
private accumulatedImages: ImagePart[] = [];
2729

@@ -42,6 +44,13 @@ export class MessageQueue {
4244
// Add text message if non-empty
4345
if (trimmedMessage.length > 0) {
4446
this.messages.push(trimmedMessage);
47+
48+
// Determine display text: use rawCommand for compaction, otherwise the message itself
49+
const muxMetadata = options?.muxMetadata as unknown;
50+
const displayText = isCompactionMetadata(muxMetadata)
51+
? muxMetadata.rawCommand
52+
: trimmedMessage;
53+
this.displayTexts.push(displayText);
4554
}
4655

4756
if (options) {
@@ -63,18 +72,11 @@ export class MessageQueue {
6372

6473
/**
6574
* Get display text for queued messages.
66-
* Returns rawCommand if this is a compaction request, otherwise joined messages.
67-
* Matches StreamingMessageAggregator behavior.
75+
* Returns all display texts joined with newlines.
76+
* Compaction requests show their rawCommand, normal messages show their text.
6877
*/
6978
getDisplayText(): string {
70-
// Check if we have compaction metadata (cast from z.any() schema type)
71-
const cmuxMetadata = this.latestOptions?.muxMetadata as unknown;
72-
if (isCompactionMetadata(cmuxMetadata)) {
73-
return cmuxMetadata.rawCommand;
74-
}
75-
76-
// Otherwise return joined messages
77-
return this.messages.join("\n");
79+
return this.displayTexts.join("\n");
7880
}
7981

8082
/**
@@ -109,6 +111,7 @@ export class MessageQueue {
109111
*/
110112
clear(): void {
111113
this.messages = [];
114+
this.displayTexts = [];
112115
this.latestOptions = undefined;
113116
this.accumulatedImages = [];
114117
}

0 commit comments

Comments
 (0)