Previously a message composed while the AI agent was streaming a reply was silently dropped (the composer early-returned on isStreaming). Now such messages are queued FIFO and sent automatically once the current turn finishes cleanly. - chat-input: submit() enqueues while streaming (via new onQueue prop) and sends otherwise; during streaming show a queue Send button (when text is present) alongside the Stop button; the textarea stays usable. - chat-thread: per-conversation queue in local state (mirrored in a ref); flush the next message in onFinish ONLY on a clean finish - ai@6 useChat fires onFinish from a finally on Stop/disconnect/error too, where the queue must be preserved. Pending messages render as removable chips above the composer. Queue is cleared on chat switch (parent remount) and survives in-place new-chat id adoption. - queue-helpers: pure FIFO helpers (enqueue/dequeue/removeQueuedById) + tests. - i18n: add en-US/ru-RU keys (Queue message, Remove queued message, Send when the agent finishes). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
108 lines
3.0 KiB
TypeScript
108 lines
3.0 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
enqueueMessage,
|
|
dequeue,
|
|
removeQueuedById,
|
|
type QueuedMessage,
|
|
} from "./queue-helpers";
|
|
|
|
describe("enqueueMessage", () => {
|
|
it("appends a message to the end of the queue", () => {
|
|
const queue: QueuedMessage[] = [{ id: "a", text: "first" }];
|
|
const next = enqueueMessage(queue, { id: "b", text: "second" });
|
|
expect(next).toEqual([
|
|
{ id: "a", text: "first" },
|
|
{ id: "b", text: "second" },
|
|
]);
|
|
});
|
|
|
|
it("does not mutate the input queue", () => {
|
|
const queue: QueuedMessage[] = [{ id: "a", text: "first" }];
|
|
enqueueMessage(queue, { id: "b", text: "second" });
|
|
expect(queue).toEqual([{ id: "a", text: "first" }]);
|
|
});
|
|
});
|
|
|
|
describe("dequeue", () => {
|
|
it("returns {head:null, rest:[]} for an empty queue", () => {
|
|
expect(dequeue([])).toEqual({ head: null, rest: [] });
|
|
});
|
|
|
|
it("returns the first item as head and the remainder as rest", () => {
|
|
const queue: QueuedMessage[] = [
|
|
{ id: "a", text: "first" },
|
|
{ id: "b", text: "second" },
|
|
{ id: "c", text: "third" },
|
|
];
|
|
const { head, rest } = dequeue(queue);
|
|
expect(head).toEqual({ id: "a", text: "first" });
|
|
expect(rest).toEqual([
|
|
{ id: "b", text: "second" },
|
|
{ id: "c", text: "third" },
|
|
]);
|
|
});
|
|
|
|
it("does not mutate the input queue", () => {
|
|
const queue: QueuedMessage[] = [
|
|
{ id: "a", text: "first" },
|
|
{ id: "b", text: "second" },
|
|
];
|
|
dequeue(queue);
|
|
expect(queue).toEqual([
|
|
{ id: "a", text: "first" },
|
|
{ id: "b", text: "second" },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("removeQueuedById", () => {
|
|
it("removes the matching id and leaves the others", () => {
|
|
const queue: QueuedMessage[] = [
|
|
{ id: "a", text: "first" },
|
|
{ id: "b", text: "second" },
|
|
{ id: "c", text: "third" },
|
|
];
|
|
const next = removeQueuedById(queue, "b");
|
|
expect(next).toEqual([
|
|
{ id: "a", text: "first" },
|
|
{ id: "c", text: "third" },
|
|
]);
|
|
});
|
|
|
|
it("returns an equivalent list when the id is not present", () => {
|
|
const queue: QueuedMessage[] = [{ id: "a", text: "first" }];
|
|
expect(removeQueuedById(queue, "missing")).toEqual([
|
|
{ id: "a", text: "first" },
|
|
]);
|
|
});
|
|
|
|
it("does not mutate the input queue", () => {
|
|
const queue: QueuedMessage[] = [
|
|
{ id: "a", text: "first" },
|
|
{ id: "b", text: "second" },
|
|
];
|
|
removeQueuedById(queue, "a");
|
|
expect(queue).toEqual([
|
|
{ id: "a", text: "first" },
|
|
{ id: "b", text: "second" },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("FIFO order", () => {
|
|
it("preserves order across enqueue -> dequeue", () => {
|
|
let queue: QueuedMessage[] = [];
|
|
queue = enqueueMessage(queue, { id: "1", text: "one" });
|
|
queue = enqueueMessage(queue, { id: "2", text: "two" });
|
|
queue = enqueueMessage(queue, { id: "3", text: "three" });
|
|
|
|
const order: string[] = [];
|
|
while (queue.length > 0) {
|
|
const { head, rest } = dequeue(queue);
|
|
if (head) order.push(head.text);
|
|
queue = rest;
|
|
}
|
|
expect(order).toEqual(["one", "two", "three"]);
|
|
});
|
|
});
|