Official
chat
Built-in chat UI (JavaScript UI plugin).
The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/chat@0.2.0
Permissions in 0.2.0
Files
chat-queue.test.ts4.1 KB
// A queued draft stays a draft, its prompt in the composer, until no chat
// runs; then the queue sends it and it becomes a running chat, one at a time.
import assert from "node:assert/strict";
import test from "node:test";
import * as Effect from "effect/Effect";
import { InputLedger, withController } from "./testing.ts";
import { onSubmit, queueChat } from "./composer.ts";
import { onChatChanged } from "./events.ts";
import { chat } from "./ui-for-tests.test.ts";
import type { Controller } from "./controller.ts";
import type { Chat } from "convergence";
const settle = Effect.promise(() => new Promise((resolve) => setTimeout(resolve, 20)));
const change = (c: Controller, id: string, overrides: Partial<Chat>) =>
onChatChanged(c, { event: "chat_changed", chat: { ...c.state.chats[id]!, ...overrides, updatedAt: new Date().toISOString() } });
const queued = (c: Controller) => c.state.chatQueue.map((entry) => entry.chatId);
test("a queued draft keeps its prompt and starts only when no chat runs", async () => {
const ledger = new InputLedger(["busy", "d1"]);
await withController(ledger, (c) =>
Effect.gen(function* () {
c.state.chats.busy = chat({ id: "busy", sessionId: "s", status: "running" });
c.state.chats.d1 = chat({ id: "d1" });
c.composer("d1").loading = false;
c.composer("d1").text = "Fix the tests";
yield* queueChat(c, "d1");
yield* settle;
assert.deepEqual(queued(c), ["d1"]);
assert.equal(c.composer("d1").text, "Fix the tests", "the prompt stays in the draft");
assert.equal(ledger.deliveries.length, 0, "another chat runs");
// Still editable while it waits: the edit is what is sent.
c.composer("d1").text = "Fix the failing tests";
yield* change(c, "busy", { status: "idle" });
yield* settle;
assert.deepEqual(queued(c), []);
assert.deepEqual(ledger.deliveries.map((d) => [d.chatId, d.blocks]), [
["d1", [{ type: "text", text: "Fix the failing tests" }]],
]);
}),
);
});
test("queued drafts start one at a time, in order", async () => {
const ledger = new InputLedger(["d1", "d2"]);
await withController(ledger, (c) =>
Effect.gen(function* () {
for (const id of ["d1", "d2"]) {
c.state.chats[id] = chat({ id });
c.composer(id).loading = false;
c.composer(id).text = `prompt ${id}`;
}
// Nothing runs: the first starts at once, the second waits for it.
yield* queueChat(c, "d1");
yield* queueChat(c, "d2");
yield* settle;
assert.deepEqual(ledger.deliveries.map((d) => d.chatId), ["d1"]);
assert.deepEqual(queued(c), ["d2"]);
yield* change(c, "d1", { status: "running" });
yield* settle;
assert.deepEqual(ledger.deliveries.map((d) => d.chatId), ["d1"]);
yield* change(c, "d1", { status: "idle", sessionId: "s1" });
yield* settle;
assert.deepEqual(ledger.deliveries.map((d) => d.chatId), ["d1", "d2"]);
assert.deepEqual(queued(c), []);
}),
);
});
test("sending a queued draft by hand takes it out of the queue", async () => {
const ledger = new InputLedger(["busy", "d1"]);
await withController(ledger, (c) =>
Effect.gen(function* () {
c.state.chats.busy = chat({ id: "busy", sessionId: "s", status: "running" });
c.state.chats.d1 = chat({ id: "d1" });
c.composer("d1").loading = false;
c.composer("d1").text = "Now";
yield* queueChat(c, "d1");
yield* onSubmit(c, "d1", { value: "Now", secondary: false, shift: false });
assert.deepEqual(queued(c), []);
assert.deepEqual(ledger.deliveries.map((d) => d.chatId), ["d1"]);
}),
);
});
test("a queue saved with its prompt puts the prompt back in the draft", async () => {
const ledger = new InputLedger(["d1"]);
ledger.storage.queues = { chats: [{ chatId: "d1", text: "Old prompt", started: false }] };
await withController(ledger, (c) =>
Effect.gen(function* () {
yield* c.restoreQueues();
assert.deepEqual(queued(c), ["d1"]);
assert.equal(c.composer("d1").text, "Old prompt");
assert.deepEqual(ledger.storage.queues, { prompts: {}, chats: [{ chatId: "d1" }] });
}),
);
});Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 105.1 KB | 5 permissions | Listed |
No comments yet.