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
composer-toolbar.test.ts4.4 KB
// The prompt toolbar: a draft queues the chat and has no prompt queue,
// typing while the agent works turns Stop into Send, and input already
// handed to the agent shows as the user's message, never as a queue card.
import assert from "node:assert/strict";
import test from "node:test";
import * as Effect from "effect/Effect";
import type { Chat, InputSnapshot, PendingInput } from "convergence";
import { renderComposer } from "./views/composer.ts";
import { renderPending } from "./views/pending.ts";
import { held } from "./testing.ts";
import { harness, chat, nodes, item, transcript } from "./ui-for-tests.test.ts";
// The catalogs a composer loads, empty.
const empty: Record<string, unknown> = {
"host/chats.options": { options: [] },
"host/chats.commands": { commands: [] },
"host/chats.skills": { skills: [] },
};
const quiet = () => harness((method) => (method in empty ? Effect.succeed(empty[method]) : undefined));
const ids = (h: ReturnType<typeof quiet>, c: Chat) =>
nodes(renderComposer(h.v, c)).map((node) => node.id).filter((id): id is string => typeof id === "string");
test("a draft queues the chat and has no prompt queue", () => {
const h = quiet();
const draft = chat({ id: "d" });
h.controller.state.chats.d = draft;
const shown = ids(h, draft);
assert.ok(shown.includes("queue-chat-d"));
assert.ok(!shown.includes("queue-d"));
});
test("a started chat queues prompts, not the chat", () => {
const h = quiet();
const started = chat({ id: "s", sessionId: "session" });
h.controller.state.chats.s = started;
const shown = ids(h, started);
assert.ok(shown.includes("queue-s"));
assert.ok(!shown.includes("queue-chat-s"));
});
test("a draft whose first prompt is starting the session is not a draft", () => {
const h = quiet();
const starting = chat({ id: "f", status: "running" });
h.controller.state.chats.f = starting;
assert.ok(!ids(h, starting).includes("queue-chat-f"));
});
test("while the agent works, Stop shows until something is typed, then Send replaces it", () => {
const h = quiet();
const running = chat({ id: "r", sessionId: "session", status: "running" });
h.controller.state.chats.r = running;
let shown = ids(h, running);
assert.ok(shown.includes("stop-r"));
assert.ok(!shown.includes("send-r"));
h.controller.composer("r").text = "also check the tests";
shown = ids(h, running);
assert.ok(shown.includes("send-r"));
assert.ok(!shown.includes("stop-r"));
});
const snapshot = (inputs: PendingInput[]): InputSnapshot => ({
chatId: "c1",
agentId: "agent",
sessionId: "session",
generation: 1,
revision: 1,
inputs,
running: true,
settling: false,
policyState: null,
});
const texts = (h: ReturnType<typeof quiet>, c: Chat) =>
renderPending(h.v, c).flatMap((card) => nodes(card)).map((node) => node.text);
test("a sent prompt the transcript already shows has no queue card while the agent picks it up", () => {
const h = quiet();
const c = chat({ id: "c1", status: "running" });
h.controller.state.chats.c1 = c;
h.controller.state.transcripts.c1 = transcript([item({ id: "sent", role: "user", text: "sent" })]);
for (const state of ["dispatching", "native_admitted", "delivery_unknown"] as const) {
h.controller.scheduler.snapshots.c1 = snapshot([held("sent", { intent: "send", originalIntent: "send", state })]);
assert.deepEqual(renderPending(h.v, c), [], state);
}
});
test("a sent prompt the agent refused keeps its card, with the error", () => {
const h = quiet();
const c = chat({ id: "c1", status: "running" });
h.controller.state.chats.c1 = c;
h.controller.state.transcripts.c1 = transcript([item({ id: "sent", role: "user", text: "sent" })]);
h.controller.scheduler.snapshots.c1 = snapshot([
held("sent", { intent: "send", originalIntent: "send", state: "rejected", error: "busy" }),
]);
const shown = texts(h, c);
assert.ok(shown.includes("Send · not admitted"));
assert.ok(shown.includes("busy"));
});
test("the prompt this composer is dispatching has no card before the agent claims it", () => {
const h = quiet();
const c = chat({ id: "c1" });
h.controller.state.chats.c1 = c;
h.controller.scheduler.snapshots.c1 = snapshot([
held("sent", { intent: "send", originalIntent: "send" }),
held("later"),
]);
h.controller.composer("c1").dispatching = "sent";
const shown = texts(h, c);
assert.ok(!shown.includes("sent"));
assert.ok(shown.includes("later"), "a real queued follow-up still shows");
});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.