diff --git a/CHANGELOG.md b/CHANGELOG.md index 77fe9718..b3ef4ee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Interrupt the AI agent and send a queued message now.** A queued AI-chat + message gains a "send now" action that interrupts the streaming turn and + immediately sends that message, keeping the agent's partial output. The + follow-up turn is tagged as an interrupt so the model is told its previous + answer was cut off and builds on it instead of restarting; the rest of the + queue still flushes normally afterward. (#198) + ## [0.94.0] - 2026-06-26 This release makes AI chat durable and fast: assistant turns are persisted to diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 57018246..44ccaa8a 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -1180,6 +1180,8 @@ "Send when the agent finishes": "Send when the agent finishes", "Queue message": "Queue message", "Remove queued message": "Remove queued message", + "Send now": "Send now", + "Interrupt and send now": "Interrupt and send now", "Stop": "Stop", "Response stopped.": "Response stopped.", "Connection lost — the answer was interrupted.": "Connection lost — the answer was interrupted.", diff --git a/apps/client/public/locales/ru-RU/translation.json b/apps/client/public/locales/ru-RU/translation.json index 1ce29237..46c19edd 100644 --- a/apps/client/public/locales/ru-RU/translation.json +++ b/apps/client/public/locales/ru-RU/translation.json @@ -723,6 +723,8 @@ "Send when the agent finishes": "Отправить, когда агент закончит", "Queue message": "Поставить в очередь", "Remove queued message": "Убрать из очереди", + "Send now": "Отправить сейчас", + "Interrupt and send now": "Прервать и отправить сейчас", "Something went wrong": "Что-то пошло не так", "Stop": "Стоп", "The AI agent could not respond. Please try again.": "AI-агент не смог ответить. Попробуйте ещё раз.", diff --git a/apps/client/src/features/ai-chat/components/chat-thread.tsx b/apps/client/src/features/ai-chat/components/chat-thread.tsx index bb194fd4..e23943d7 100644 --- a/apps/client/src/features/ai-chat/components/chat-thread.tsx +++ b/apps/client/src/features/ai-chat/components/chat-thread.tsx @@ -217,11 +217,14 @@ export default function ChatThread({ const interruptNextSendRef = useRef(false); // FIFO dequeue + send the next queued message (no-op when the queue is empty). + // Returns whether a message was actually sent, so callers can tell an empty + // dequeue (nothing to flush) from a real send. const flushNext = useCallback(() => { const { head, rest } = dequeue(queuedRef.current); - if (!head) return; + if (!head) return false; setQueue(rest); sendMessageRef.current?.({ text: head.text }); + return true; }, [setQueue]); const enqueue = useCallback( @@ -309,7 +312,11 @@ export default function ChatThread({ flushOnAbortRef.current = false; // Suppress the "Response stopped." flash for an intentional interrupt. setStopNotice(null); - flushNext(); + // If the promoted head vanished (e.g. the user removed it before the + // abort landed) flushNext sends nothing — clear the one-shot interrupt + // tag so it can't leak onto the next unrelated send. On a real send the + // tag is consumed by prepareSendMessagesRequest and stays untouched. + if (!flushNext()) interruptNextSendRef.current = false; return; } if (isAbort || isDisconnect || isError) return; diff --git a/apps/server/src/core/ai-chat/ai-chat.prompt.ts b/apps/server/src/core/ai-chat/ai-chat.prompt.ts index ba7ff326..f0a9c2d0 100644 --- a/apps/server/src/core/ai-chat/ai-chat.prompt.ts +++ b/apps/server/src/core/ai-chat/ai-chat.prompt.ts @@ -72,8 +72,6 @@ const INTERRUPT_NOTE = 'assume your previous response was complete, and do not silently restart the ' + 'partial work — build on it or follow the new instruction.'; -export { INTERRUPT_NOTE }; - export interface BuildSystemPromptInput { workspace: Workspace; /**