Add a "send now" button to queued AI-chat messages: it interrupts the running agent and immediately sends that message, while the agent's partial output at interruption is kept in history and the next turn is marked as a user interrupt. Client: - queue-helpers: pure `promoteToHead` to move a queued message to the head. - chat-thread: `sendNow` (promote head + abort + flush-on-abort), one-shot `flushOnAbortRef`/`interruptNextSendRef`, `interrupted` flag in the request body, and the "send now" ActionIcon in the queued list. Server: - `interrupted` on AiChatStreamBody; pure `isInterruptResume` confirms the client hint against persisted history (prev assistant turn aborted/ streaming) before honouring it. - prompt: INTERRUPT_NOTE injected in the context section only on a confirmed interrupt-resume turn so the model treats the partial answer above as incomplete. Tests: promoteToHead, chat-thread send-now (abort + resend + one-shot interrupt flag + non-streaming immediate send), isInterruptResume, and the prompt interrupt-note injection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
48 lines
1.5 KiB
TypeScript
48 lines
1.5 KiB
TypeScript
// Pure FIFO helpers for the AI-chat "send while the agent is busy" queue.
|
|
// Kept side-effect free so they can be unit-tested without React.
|
|
|
|
export interface QueuedMessage {
|
|
id: string;
|
|
text: string;
|
|
}
|
|
|
|
/** Append a message to the end of the queue (returns a new array). */
|
|
export function enqueueMessage(
|
|
queue: QueuedMessage[],
|
|
message: QueuedMessage,
|
|
): QueuedMessage[] {
|
|
return [...queue, message];
|
|
}
|
|
|
|
/** Split the queue into its first item (`head`) and the remainder (`rest`).
|
|
* `head` is null when the queue is empty. Does not mutate the input. */
|
|
export function dequeue(queue: QueuedMessage[]): {
|
|
head: QueuedMessage | null;
|
|
rest: QueuedMessage[];
|
|
} {
|
|
if (queue.length === 0) return { head: null, rest: [] };
|
|
const [head, ...rest] = queue;
|
|
return { head, rest };
|
|
}
|
|
|
|
/** Remove the queued message with the given id (returns a new array). */
|
|
export function removeQueuedById(
|
|
queue: QueuedMessage[],
|
|
id: string,
|
|
): QueuedMessage[] {
|
|
return queue.filter((m) => m.id !== id);
|
|
}
|
|
|
|
/** Move the queued message with the given id to the FRONT (returns a new array).
|
|
* No-op (returns an equivalent array) when the id is absent. Pure — backs the
|
|
* "send now" action: promoting a message to the head lets the existing
|
|
* onFinish -> flushNext path send exactly that message on the abort we trigger. */
|
|
export function promoteToHead(
|
|
queue: QueuedMessage[],
|
|
id: string,
|
|
): QueuedMessage[] {
|
|
const target = queue.find((m) => m.id === id);
|
|
if (!target) return queue;
|
|
return [target, ...queue.filter((m) => m.id !== id)];
|
|
}
|