Skip to content

Commit a1ee7e1

Browse files
committed
fix: address PR review feedback for queued messages
- Export `enqueue` from `createChatRunService` - Provide empty metadata block to runs created via legacy `/api/chat` to populate `conversationId` - Mark `nextRun.status = 'starting'` before invoking async preflight in `maybeStartNext` to prevent a race condition - Update `ChatComposer.tsx` to not short-circuit submit when streaming - Add `chat.queue` to `Dict` and all locales for the Queue button text
1 parent 37a1264 commit a1ee7e1

21 files changed

Lines changed: 50 additions & 8 deletions

File tree

apps/daemon/src/runs.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,23 +79,26 @@ export function createChatRunService({
7979
.sort((a, b) => a.createdAt - b.createdAt);
8080

8181
// Check if any run is currently active for this conversation.
82-
const activeRun = conversationRuns.find((r) => !TERMINAL_RUN_STATUSES.has(r.status) && r.status !== 'queued');
82+
const activeRun = conversationRuns.find((r) => (!TERMINAL_RUN_STATUSES.has(r.status) && r.status !== 'queued') || r._isStarting);
8383
if (activeRun) return;
8484

8585
// Find the oldest queued run.
86-
const nextRun = conversationRuns.find((r) => r.status === 'queued');
86+
const nextRun = conversationRuns.find((r) => r.status === 'queued' && !r._isStarting);
8787
if (nextRun) {
8888
const starter = starters.get(nextRun.id);
8989
if (starter) {
9090
// Remove from starters map once we're about to run it
9191
starters.delete(nextRun.id);
92+
// Mark as starting to prevent race condition while async preflight runs
93+
nextRun._isStarting = true;
9294
start(nextRun, starter);
9395
}
9496
}
9597
};
9698

9799
const finish = (run, status, code = null, signal = null) => {
98100
if (TERMINAL_RUN_STATUSES.has(run.status)) return;
101+
run._isStarting = false;
99102
run.status = status;
100103
run.exitCode = code;
101104
run.signal = signal;
@@ -117,12 +120,15 @@ export function createChatRunService({
117120
const start = (run, starter) => {
118121
void starter(run).catch((err) => {
119122
fail(run, 'AGENT_EXECUTION_FAILED', err instanceof Error ? err.message : String(err));
123+
}).finally(() => {
124+
run._isStarting = false;
120125
});
121126
return run;
122127
};
123128

124129
const enqueue = (run, starter) => {
125130
if (run.status !== 'queued') return run; // Only enqueue fresh runs
131+
if (!run.conversationId) return start(run, starter);
126132
starters.set(run.id, starter);
127133
maybeStartNext(run.conversationId);
128134
return run;
@@ -172,6 +178,7 @@ export function createChatRunService({
172178
return {
173179
create,
174180
start,
181+
enqueue,
175182
get,
176183
list,
177184
stream,

apps/daemon/src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3942,7 +3942,7 @@ export async function startServer({ port = 7456, host = process.env.OD_BIND_HOST
39423942
});
39433943

39443944
app.post('/api/chat', (req, res) => {
3945-
const run = design.runs.create();
3945+
const run = design.runs.create(req.body || {});
39463946
design.runs.stream(run, req, res);
39473947
design.runs.enqueue(run, () => startChatRun(req.body || {}, run));
39483948
});

apps/web/src/components/ChatComposer.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -488,12 +488,11 @@ export const ChatComposer = forwardRef<ChatComposerHandle, Props>(
488488
// adopts it from "Recently hatched" in pet settings afterwards.
489489
const hatched = expandHatchCommand(prompt);
490490
if (hatched) {
491-
if (streaming) return;
492491
onSend(hatched, staged, commentAttachments);
493492
reset();
494493
return;
495494
}
496-
if ((!prompt && commentAttachments.length === 0) || streaming) return;
495+
if (!prompt && commentAttachments.length === 0) return;
497496
onSend(prompt, staged, commentAttachments);
498497
reset();
499498
}
@@ -809,7 +808,7 @@ export const ChatComposer = forwardRef<ChatComposerHandle, Props>(
809808
disabled={!draft.trim() && commentAttachments.length === 0}
810809
>
811810
<Icon name="send" size={13} />
812-
<span>{streaming ? 'Queue' : t('chat.send')}</span>
811+
<span>{streaming ? t('chat.queue') : t('chat.send')}</span>
813812
</button>
814813
</div>
815814
</div>

apps/web/src/components/ProjectView.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ export function ProjectView({
192192
prompt: string;
193193
attachments: ChatAttachment[];
194194
commentAttachments: ChatCommentAttachment[];
195+
projectId: string;
196+
conversationId: string;
195197
} | null>(null);
196198
const abortRef = useRef<AbortController | null>(null);
197199
const cancelRef = useRef<AbortController | null>(null);
@@ -854,6 +856,16 @@ export function ProjectView({
854856
if (!activeConversationId) return;
855857
if (streaming) return;
856858
if (!prompt.trim() && attachments.length === 0 && commentAttachments.length === 0) return;
859+
if (streaming) {
860+
setQueuedMessage({
861+
prompt,
862+
attachments,
863+
commentAttachments,
864+
projectId: project.id,
865+
conversationId: activeConversationId,
866+
});
867+
return;
868+
}
857869
setError(null);
858870
const startedAt = Date.now();
859871
const userMsg: ChatMessage = {
@@ -1207,14 +1219,22 @@ export function ProjectView({
12071219
// Dispatch queued messages after the current run finishes.
12081220
useEffect(() => {
12091221
if (!streaming && queuedMessage) {
1222+
if (
1223+
queuedMessage.projectId !== project.id ||
1224+
queuedMessage.conversationId !== activeConversationId
1225+
) {
1226+
// Clear if the user switched contexts before the stream finished
1227+
setQueuedMessage(null);
1228+
return;
1229+
}
12101230
const { prompt, attachments, commentAttachments } = queuedMessage;
12111231
setQueuedMessage(null);
12121232
// Wait a tick so state has settled (streaming=false) before dispatching
12131233
setTimeout(() => {
12141234
void handleSend(prompt, attachments, commentAttachments);
12151235
}, 0);
12161236
}
1217-
}, [streaming, queuedMessage, handleSend]);
1237+
}, [streaming, queuedMessage, project.id, activeConversationId, handleSend]);
12181238

12191239
const persistArtifact = useCallback(
12201240
async (art: Artifact) => {
@@ -1527,7 +1547,6 @@ export function ProjectView({
15271547
onRequestOpenFile={requestOpenFile}
15281548
initialDraft={initialDraft}
15291549
onSubmitForm={(text) => {
1530-
if (streaming) return;
15311550
void handleSend(text, [], []);
15321551
}}
15331552
onContinueRemainingTasks={handleContinueRemainingTasks}

apps/web/src/i18n/locales/ar.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ export const ar: Dict = {
451451
'chat.linkedFolderAlready': 'هذا المجلد مرتبط بالفعل',
452452
'chat.linkedFolderPickError': 'تعذر فتح منتقي المجلدات',
453453
'chat.send': 'إرسال',
454+
'chat.queue': 'Queue',
454455
'chat.stop': 'إيقاف',
455456
'chat.removeAria': 'إزالة {name}',
456457
'chat.example1Title': 'عرض تقديمي تحريري',

apps/web/src/i18n/locales/de.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ export const de: Dict = {
405405
'chat.linkedFolderAlready': 'Dieser Ordner ist bereits verknüpft',
406406
'chat.linkedFolderPickError': 'Ordnerauswahl konnte nicht geöffnet werden',
407407
'chat.send': 'Senden',
408+
'chat.queue': 'Queue',
408409
'chat.stop': 'Stoppen',
409410
'chat.removeAria': '{name} entfernen',
410411
'chat.example1Title': 'Editorial Pitch Deck',

apps/web/src/i18n/locales/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ export const en: Dict = {
462462
'chat.linkedFolderAlready': 'This folder is already linked',
463463
'chat.linkedFolderPickError': 'Could not open folder picker',
464464
'chat.send': 'Send',
465+
'chat.queue': 'Queue',
465466
'chat.stop': 'Stop',
466467
'chat.removeAria': 'Remove {name}',
467468
'chat.example1Title': 'Editorial pitch deck',

apps/web/src/i18n/locales/es-ES.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ export const esES: Dict = {
406406
'chat.linkedFolderAlready': 'Esta carpeta ya está vinculada',
407407
'chat.linkedFolderPickError': 'No se pudo abrir el selector de carpetas',
408408
'chat.send': 'Enviar',
409+
'chat.queue': 'Queue',
409410
'chat.stop': 'Detener',
410411
'chat.removeAria': 'Quitar {name}',
411412
'chat.example1Title': 'Pitch deck editorial',

apps/web/src/i18n/locales/fa.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ export const fa: Dict = {
462462
'chat.linkedFolderAlready': 'این پوشه قبلاً لینک شده است',
463463
'chat.linkedFolderPickError': 'انتخابگر پوشه باز نشد',
464464
'chat.send': 'ارسال',
465+
'chat.queue': 'Queue',
465466
'chat.stop': 'توقف',
466467
'chat.removeAria': 'حذف {name}',
467468
'chat.example1Title': 'ارائه سردبیری',

apps/web/src/i18n/locales/fr.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ export const fr: Dict = {
451451
'chat.linkedFolderAlready': 'Ce dossier est déjà lié',
452452
'chat.linkedFolderPickError': 'Impossible d\'ouvrir le sélecteur de dossier',
453453
'chat.send': 'Envoyer',
454+
'chat.queue': 'Queue',
454455
'chat.stop': 'Arrêter',
455456
'chat.removeAria': 'Retirer {name}',
456457
'chat.example1Title': 'Pitch deck éditorial',

0 commit comments

Comments
 (0)