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
usage.test.ts5 KB
// The limits and context meters under the prompt box, and the combined model picker.
import assert from "node:assert/strict";
import test from "node:test";
import type { AgentInfo } from "convergence";
import { headlineWindows, resetsIn, ringCommands, usageMeter, contextMeter } from "./views/usage.ts";
import { harness, tree as inspect, nodes, chat as makeChat } from "./ui-for-tests.test.ts";
const texts = (element: unknown) => nodes(element).flatMap((node) => (node.text ? [node.text] : []));
const agent = (fields: Partial<AgentInfo>) =>
({ id: "one", name: "One", capabilities: { usageLimits: true }, status: { state: "ready" }, ...fields }) as AgentInfo;
test("an agent that reports limits always gets a meter, empty until the first report", () => {
const h = harness();
const chat = makeChat({ id: "c", agentId: "one" });
h.controller.state.agents.one = agent({});
assert.deepEqual(texts(usageMeter(h.v, chat)).slice(0, 1), ["—"]);
h.controller.state.agents.one = agent({
usageLimits: {
windows: [
{ id: "five_hour", label: "5 hours", usedPercent: 23.4, windowMinutes: 300 },
{ id: "seven_day", label: "7 days", usedPercent: 91, windowMinutes: 10080 },
],
},
});
// Beside the prompt: rings and their lengths, never the share. The
// numbers are a press away in the card.
assert.deepEqual(texts(usageMeter(h.v, chat)).slice(0, 2), ["5h", "7d"]);
// Beside the prompt, one window per length: a model's own weekly window
// and windows of no fixed length are in the card only.
assert.deepEqual(
headlineWindows([
{ id: "five_hour", label: "5 hours", usedPercent: 9, windowMinutes: 300 },
{ id: "overage", label: "Extra Usage", usedPercent: 0 },
{ id: "seven_day", label: "7 days", usedPercent: 37, windowMinutes: 10080 },
{ id: "seven_day_fable", label: "7 days (Fable)", usedPercent: 0, windowMinutes: 10080 },
]).map((window) => window.id),
["five_hour", "seven_day"],
);
// Below 10% the card keeps one decimal.
h.controller.state.agents.one = agent({
usageLimits: { windows: [{ id: "five_hour", label: "5 hours", usedPercent: 0.4, windowMinutes: 300 }] },
});
assert.equal(texts(usageMeter(h.v, chat))[0], "5h");
assert.ok(texts(usageMeter(h.v, chat)).includes("0.4% used"));
// A spend allowance alone still gives the card a trigger.
assert.deepEqual(
headlineWindows([{ id: "overage", label: "Extra Usage", usedPercent: 12 }]).map((window) => window.id),
["overage"],
);
h.controller.state.agents.one = agent({ capabilities: {} });
assert.equal(usageMeter(h.v, chat), null);
});
test("the context meter is a ring, the share and its name", () => {
const h = harness();
const chat = makeChat({ id: "c" });
assert.equal(contextMeter(h.v, chat), null);
h.controller.transcript("c").usage = { usedTokens: 25_000, contextWindow: 100_000 };
assert.deepEqual(texts(contextMeter(h.v, chat)).slice(0, 2), ["25%", "Ctx"]);
assert.ok(nodes(contextMeter(h.v, chat)).some((node) => node.type === "canvas"));
});
test("the windows under the prompt are set apart from one another", () => {
const h = harness();
const chat = makeChat({ id: "c", agentId: "one" });
const windows = [
{ id: "five_hour", label: "5 hours", usedPercent: 23.4, windowMinutes: 300 },
{ id: "seven_day", label: "7 days", usedPercent: 91, windowMinutes: 10080 },
];
h.controller.state.agents.one = agent({ usageLimits: { windows } });
const strip = () => nodes(usageMeter(h.v, chat)).find((node) => node.type === "div" && node.children?.every((child) => child.type === "div" && child.children.some((part) => part.type === "canvas")))!;
const at = () => strip().children;
assert.equal(at().length, 2);
// Ring to length stays tight; window to window opens up, so a 5h ring
// is never read as the 7d one's.
assert.ok(Number(strip().gap) > Number(at()[0].gap));
h.controller.state.agents.one = agent({ usageLimits: { windows: windows.slice(0, 1) } });
assert.equal(at().length, 1);
assert.equal(Number(strip().gap), Number(at()[0].gap));
});
test("reset times read as the time left", () => {
const now = Date.parse("2026-01-01T00:00:00Z");
assert.equal(resetsIn("2026-01-01T02:05:00Z", now), "2h 5m");
assert.equal(resetsIn("2026-01-03T03:00:00Z", now), "2d 3h");
assert.equal(resetsIn("2025-12-31T00:00:00Z", now), "now");
assert.equal(resetsIn(undefined, now), null);
});
test("a limit's ring is a full faint track and an arc clockwise from the top for the share used", () => {
const [track, arc] = ringCommands(0.25, "warning") as { op: string; d: (string | number)[][]; stroke: string }[];
assert.equal(track.stroke, "mutedForeground/25");
assert.equal(arc.stroke, "warning");
const end = (d: (string | number)[][]) => d.at(-1)!.slice(-2).map((n) => Math.round(Number(n) * 100) / 100);
assert.deepEqual(arc.d[0].map((n) => (typeof n === "number" ? Math.round(n * 100) / 100 : n)), ["moveTo", 7, 1]);
assert.deepEqual(end(arc.d), [13, 7]);
assert.deepEqual(end(track.d), [7, 1]);
assert.equal(ringCommands(0, "danger").length, 1);
});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.