Skip to content

Commit 10d1359

Browse files
fix(off-grid-ai#510): carry imageMode through the queue so a queued force-image send draws
A force-image send that queued behind an in-flight generation lost its force flag: QueuedMessage carried no imageMode, so on drain handleQueuedSend → dispatchGenerationFn re-decided modality at imageMode='auto' → resolveTurnKind classified the (non-draw) text as TEXT. A message the user explicitly forced to image generated as a text reply. Carry imageMode end-to-end: add it to QueuedMessage, set it when handleSendFn enqueues, preserve it in the processNextInQueue merge (any coalesced force wins), and pass it through handleQueuedSend to dispatchGenerationFn. A queued force-image send now dispatches as image (trace: [ROUTE-SM] dispatch imageMode=force → IMAGE pipeline), not re-classified at 'auto'.
1 parent 2e567df commit 10d1359

3 files changed

Lines changed: 14 additions & 3 deletions

File tree

src/screens/ChatScreen/useChatGenerationActions.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ export async function dispatchGenerationFn(
494494
}
495495
export type SendCall = { text: string; attachments?: MediaAttachment[]; imageMode?: 'auto' | 'force' | 'disabled'; startGeneration: (convId: string, text: string) => Promise<void>; setDebugInfo: SetState<any> };
496496
export async function handleSendFn(deps: GenerationDeps, call: SendCall): Promise<void> {
497-
const { text, attachments, imageMode, startGeneration } = call;
497+
const { text, attachments, imageMode = 'auto', startGeneration } = call;
498498
abortPreload(); // user acted — stop background warming so it can't block them
499499
if (!deps.hasActiveModel) { deps.setAlertState(showAlert('No Model Selected', 'Please select a model first.')); return; }
500500
// Vision gate (shared with resend): never send an image to a model that can't do vision.
@@ -510,7 +510,9 @@ export async function handleSendFn(deps: GenerationDeps, call: SendCall): Promis
510510
// Cross-modality serialization: queue if any generation is running (routed later).
511511
if (generationService.getState().isGenerating || imageGenerationService.getState().isGenerating) {
512512
const messageText = appendAttachmentText(text, attachments);
513-
generationService.enqueueMessage({ id: nextMsgId(), conversationId: targetConversationId, text, attachments, messageText });
513+
// Carry the user's forced modality through the queue so a queued force-image send is dispatched as
514+
// image on drain — not re-decided at 'auto' by resolveTurnKind (#510).
515+
generationService.enqueueMessage({ id: nextMsgId(), conversationId: targetConversationId, text, attachments, messageText, imageMode });
514516
return;
515517
}
516518
await dispatchGenerationFn(deps, { text, attachments, conversationId: targetConversationId, imageMode }, startGeneration);

src/screens/ChatScreen/useChatScreen.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,10 @@ export const useChatScreen = () => {
257257

258258
// Drain queued messages through the same routing layer as a fresh send.
259259
const handleQueuedSend = useCallback(async (item: QueuedMessage) => {
260+
// Pass the queued send's forced modality (imageMode) so a message the user forced to image mode
261+
// is dispatched as image, not re-decided at 'auto' by resolveTurnKind (#510).
260262
await dispatchGenerationFn(genDepsRef.current,
261-
{ text: item.text, attachments: item.attachments, conversationId: item.conversationId }, startGenerationRef.current);
263+
{ text: item.text, attachments: item.attachments, conversationId: item.conversationId, imageMode: item.imageMode }, startGenerationRef.current);
262264
}, []);
263265

264266
useEffect(() => {

src/services/generationService.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ type StreamChunk = string | { content?: string; reasoningContent?: string };
2525
export interface QueuedMessage {
2626
id: string; conversationId: string; text: string;
2727
attachments?: MediaAttachment[]; messageText: string;
28+
/** The modality the user forced for THIS send (force/disabled/auto). Carried through the queue so a
29+
* message the user explicitly forced to image mode is dispatched as image on drain — never re-decided
30+
* at 'auto' by resolveTurnKind (#510: a queued force-image send generated as text). */
31+
imageMode?: 'auto' | 'force' | 'disabled';
2832
}
2933

3034
export interface GenerationState {
@@ -346,6 +350,9 @@ class GenerationService {
346350
text: all.map(m => m.text).join('\n\n'),
347351
attachments: all.flatMap(m => m.attachments || []),
348352
messageText: all.map(m => m.messageText).join('\n\n'),
353+
// If ANY coalesced send forced image mode, the combined dispatch must force image too — the
354+
// user's explicit force must never be dropped by the merge (mirror of the single-message carry).
355+
imageMode: all.some(m => m.imageMode === 'force') ? 'force' : all[0].imageMode,
349356
};
350357
this.queueProcessor(combined).catch(e => { logger.error('[GenerationService] Queue processor error:', e); });
351358
}

0 commit comments

Comments
 (0)