11import type { ImagePart , SendMessageOptions } from "@/common/orpc/types" ;
2+ import type { CompactionRequestData , ContinueMessage } from "@/common/types/message" ;
23
3- // Type guard for compaction request metadata ( for display text)
4+ // Full compaction metadata structure for manipulation
45interface CompactionMetadata {
56 type : "compaction-request" ;
67 rawCommand : string ;
8+ parsed : CompactionRequestData ;
79}
810
911function isCompactionMetadata ( meta : unknown ) : meta is CompactionMetadata {
@@ -20,19 +22,42 @@ function isCompactionMetadata(meta: unknown): meta is CompactionMetadata {
2022 * - Latest options (model, thinking level, etc. - overwrites on each add)
2123 * - Image parts (accumulated across all messages)
2224 *
25+ * Special handling for compaction requests:
26+ * - Adding a compaction request to a non-empty queue throws an error
27+ * (must wait for current messages to send before compacting)
28+ * - Adding a message AFTER a compaction request appends it to the
29+ * compaction's continueMessage (so it's sent after compaction completes)
30+ *
2331 * Display logic:
24- * - Single compaction request → shows rawCommand (/compact)
25- * - Multiple messages → shows all actual message texts (since compaction metadata is lost anyway)
32+ * - Compaction request alone → shows rawCommand (/compact)
33+ * - Compaction with follow-ups → shows rawCommand + follow-up texts
34+ * - Regular messages → shows all message texts
2635 */
2736export class MessageQueue {
2837 private messages : string [ ] = [ ] ;
2938 private latestOptions ?: SendMessageOptions ;
3039 private accumulatedImages : ImagePart [ ] = [ ] ;
40+ // Track follow-up messages added after a compaction request
41+ // These get appended to the compaction's continueMessage
42+ private compactionFollowUps : string [ ] = [ ] ;
43+ private compactionFollowUpImages : ImagePart [ ] = [ ] ;
44+
45+ /**
46+ * Check if the queue currently contains a compaction request.
47+ */
48+ hasCompactionRequest ( ) : boolean {
49+ return isCompactionMetadata ( this . latestOptions ?. muxMetadata ) ;
50+ }
3151
3252 /**
3353 * Add a message to the queue.
3454 * Updates to latest options, accumulates image parts.
3555 * Allows image-only messages (empty text with images).
56+ *
57+ * Special case: Adding a message after a compaction request appends it
58+ * to the compaction's continueMessage rather than overwriting options.
59+ *
60+ * @throws Error if trying to add a compaction request when queue already has messages
3661 */
3762 add ( message : string , options ?: SendMessageOptions & { imageParts ?: ImagePart [ ] } ) : void {
3863 const trimmedMessage = message . trim ( ) ;
@@ -43,7 +68,32 @@ export class MessageQueue {
4368 return ;
4469 }
4570
46- // Add text message if non-empty
71+ const incomingIsCompaction = isCompactionMetadata ( options ?. muxMetadata ) ;
72+ const queueHasCompaction = this . hasCompactionRequest ( ) ;
73+ const queueHasMessages = ! this . isEmpty ( ) ;
74+
75+ // Cannot add compaction to a queue that already has messages
76+ // (user should wait for those messages to send first)
77+ if ( incomingIsCompaction && queueHasMessages ) {
78+ throw new Error (
79+ "Cannot queue compaction request: queue already has messages. " +
80+ "Wait for current stream to complete before compacting."
81+ ) ;
82+ }
83+
84+ // Special case: adding follow-up message after a pending compaction
85+ // Append to the compaction's continueMessage instead of overwriting options
86+ if ( ! incomingIsCompaction && queueHasCompaction ) {
87+ if ( trimmedMessage . length > 0 ) {
88+ this . compactionFollowUps . push ( trimmedMessage ) ;
89+ }
90+ if ( hasImages && options ?. imageParts ) {
91+ this . compactionFollowUpImages . push ( ...options . imageParts ) ;
92+ }
93+ return ;
94+ }
95+
96+ // Normal case: add message to queue
4797 if ( trimmedMessage . length > 0 ) {
4898 this . messages . push ( trimmedMessage ) ;
4999 }
@@ -60,21 +110,29 @@ export class MessageQueue {
60110
61111 /**
62112 * Get all queued message texts (for editing/restoration).
113+ * Includes follow-up messages if compaction is pending.
63114 */
64115 getMessages ( ) : string [ ] {
116+ if ( this . compactionFollowUps . length > 0 ) {
117+ return [ ...this . messages , ...this . compactionFollowUps ] ;
118+ }
65119 return [ ...this . messages ] ;
66120 }
67121
68122 /**
69123 * Get display text for queued messages.
70- * - Single compaction request shows rawCommand (/compact)
71- * - Multiple messages or non-compaction show actual message texts
124+ * - Compaction alone → shows rawCommand (/compact)
125+ * - Compaction with follow-ups → shows rawCommand + newline + follow-ups
126+ * - Regular messages → shows all message texts
72127 */
73128 getDisplayText ( ) : string {
74- // Only show rawCommand for single compaction request
75- // (compaction metadata is only preserved when no follow-up messages are added)
76129 const muxMetadata = this . latestOptions ?. muxMetadata as unknown ;
77- if ( this . messages . length === 1 && isCompactionMetadata ( muxMetadata ) ) {
130+
131+ if ( isCompactionMetadata ( muxMetadata ) ) {
132+ // Show compaction command plus any follow-ups
133+ if ( this . compactionFollowUps . length > 0 ) {
134+ return muxMetadata . rawCommand + "\n" + this . compactionFollowUps . join ( "\n" ) ;
135+ }
78136 return muxMetadata . rawCommand ;
79137 }
80138
@@ -83,19 +141,71 @@ export class MessageQueue {
83141
84142 /**
85143 * Get accumulated image parts for display.
144+ * Includes follow-up images if compaction is pending.
86145 */
87146 getImageParts ( ) : ImagePart [ ] {
147+ if ( this . compactionFollowUpImages . length > 0 ) {
148+ return [ ...this . accumulatedImages , ...this . compactionFollowUpImages ] ;
149+ }
88150 return [ ...this . accumulatedImages ] ;
89151 }
90152
91153 /**
92154 * Get combined message and options for sending.
93- * Returns joined messages with latest options + accumulated images.
155+ *
156+ * For compaction requests with follow-ups:
157+ * - Returns the compaction message unchanged
158+ * - Merges follow-ups into the continueMessage field of the metadata
159+ *
160+ * For regular messages:
161+ * - Returns joined messages with latest options + accumulated images
94162 */
95163 produceMessage ( ) : {
96164 message : string ;
97165 options ?: SendMessageOptions & { imageParts ?: ImagePart [ ] } ;
98166 } {
167+ // Cast from z.any() schema type to unknown for safe type narrowing
168+ const muxMetadata = this . latestOptions ?. muxMetadata as unknown ;
169+
170+ // Special handling for compaction with follow-ups
171+ if ( isCompactionMetadata ( muxMetadata ) && this . hasFollowUps ( ) ) {
172+ const existingContinue = muxMetadata . parsed . continueMessage ;
173+ const existingText = existingContinue ?. text ?? "" ;
174+ const existingImages = existingContinue ?. imageParts ?? [ ] ;
175+
176+ // Merge follow-ups with existing continueMessage
177+ const mergedText = existingText
178+ ? existingText + "\n" + this . compactionFollowUps . join ( "\n" )
179+ : this . compactionFollowUps . join ( "\n" ) ;
180+ const mergedImages = [ ...existingImages , ...this . compactionFollowUpImages ] ;
181+
182+ const newContinueMessage : ContinueMessage = {
183+ text : mergedText ,
184+ imageParts : mergedImages . length > 0 ? mergedImages : undefined ,
185+ model : existingContinue ?. model ,
186+ } ;
187+
188+ // Create updated metadata with merged continueMessage
189+ const updatedMetadata : CompactionMetadata = {
190+ type : "compaction-request" ,
191+ rawCommand : muxMetadata . rawCommand ,
192+ parsed : {
193+ ...muxMetadata . parsed ,
194+ continueMessage : newContinueMessage ,
195+ } ,
196+ } ;
197+
198+ return {
199+ message : this . messages . join ( "\n" ) ,
200+ options : {
201+ ...this . latestOptions ! ,
202+ muxMetadata : updatedMetadata ,
203+ imageParts : this . accumulatedImages . length > 0 ? this . accumulatedImages : undefined ,
204+ } ,
205+ } ;
206+ }
207+
208+ // Normal case
99209 const joinedMessages = this . messages . join ( "\n" ) ;
100210
101211 const options = this . latestOptions
@@ -108,13 +218,22 @@ export class MessageQueue {
108218 return { message : joinedMessages , options } ;
109219 }
110220
221+ /**
222+ * Check if there are follow-up messages pending after a compaction.
223+ */
224+ private hasFollowUps ( ) : boolean {
225+ return this . compactionFollowUps . length > 0 || this . compactionFollowUpImages . length > 0 ;
226+ }
227+
111228 /**
112229 * Clear all queued messages, options, and images.
113230 */
114231 clear ( ) : void {
115232 this . messages = [ ] ;
116233 this . latestOptions = undefined ;
117234 this . accumulatedImages = [ ] ;
235+ this . compactionFollowUps = [ ] ;
236+ this . compactionFollowUpImages = [ ] ;
118237 }
119238
120239 /**
0 commit comments