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

  • Control chats chats.controlHighCreates chats, sends prompts and cancels runs.Send prompts, answer agent questions, and create, change, fork, revert, stop and group chats
  • Run other plugins' commands commands.runHighRuns commands of other plugins.Run the sidebar's New chat command from the title bar
  • Read chats chats.readMediumReads your transcripts and chat lists.Show workspaces, tasks, chats and transcripts, and follow them as agents work
  • Read files fs.readMediumReads files in the listed places.Suggest workspace files for @ mentionsPlaces: the open workspace
  • Show panels ui.slotsLowShows views in the listed parts of the window.Draw the chat grid and the title barSlots: centertitle

Files

subagents.test.ts5.5 KB
// Regression coverage for subagents using the real chat modules.
// Fixtures exercise the same behavior and assertions as the JavaScript tests.
import { item as fixtureItem, fixtureTask, taskItemAt, entryItems } from "./ui-for-tests.test.ts";
import assert from "node:assert/strict";
import test from "node:test";

import {
  applyTask,
  findTask,
  withoutSpawnRows,
  groupTasks,
  allTasks,
  counts,
  countsLabel,
  groupLabel,
  elapsed,
  tokens,
  metaLine,
  resultOf,
  isLive,
} from "./subagents.ts";

const task = fixtureTask;

test("a subagent goes inside the one that started it, at any depth", () => {
  const items: import("convergence").TranscriptItem[] = [];
  applyTask(items, task("a"));
  applyTask(items, task("b", { parentTaskId: "a" }));
  applyTask(items, task("c", { parentTaskId: "b" }));
  assert.equal(items.length, 1);
  assert.equal(findTask(items, "c")?.task.id, "c");
  assert.equal(findTask(items, "b")?.items?.[0].id, "task-c");
});

test("a later snapshot updates the task in place and keeps its items", () => {
  const items: import("convergence").TranscriptItem[] = [];
  applyTask(items, task("a"));
  findTask(items, "a")?.items?.push(fixtureItem({ id: "m", role: "assistant", text: "hi" }));
  applyTask(items, task("a", { status: "completed", summary: "done" }));
  assert.equal(items.length, 1);
  assert.equal(taskItemAt(items, 0).task.status, "completed");
  assert.equal(taskItemAt(items, 0).items?.length, 1);
});

test("the tool row that started a subagent is hidden, other tools stay", () => {
  const items = [
    fixtureItem({ id: "t1", role: "tool", call: { id: "t1" } }),
    fixtureItem({ id: "t2", role: "tool", call: { id: "t2" } }),
    fixtureItem({ id: "task-x", role: "task", task: task("x", { toolCallId: "t1" }), items: [] }),
  ];
  assert.deepEqual(
    withoutSpawnRows(items).map((i) => i.id),
    ["t2", "task-x"],
  );
});

test("two or more subagents in a row form a group; one alone does not", () => {
  const t = (id: string) => ({
    kind: "item" as const,
    item: fixtureItem({ id, role: "task", task: task(id), items: [] }),
  });
  const text = { kind: "item" as const, item: fixtureItem({ id: "m", role: "assistant" }) };
  const out = groupTasks([t("a"), t("b"), text, t("c")]);
  assert.equal(out.length, 3);
  assert.equal(out[0].kind, "tasks");
  assert.deepEqual(
    entryItems(out[0]).map((i) => i.id),
    ["a", "b"],
  );
  assert.equal(out[2].kind, "item");
});

test("a workflow keeps its own card: it never joins a group of subagents", () => {
  const t = (id: string, extra: Parameters<typeof fixtureTask>[1] = {}) => ({
    kind: "item" as const,
    item: fixtureItem({ id, role: "task", task: task(id, extra), items: [] }),
  });
  const out = groupTasks([t("a"), t("wf", { workflow: { phases: [], logs: [] } }), t("b"), t("c")]);
  assert.deepEqual(
    out.map((entry) => entry.kind),
    ["item", "item", "tasks"],
  );
  assert.equal(out[1].kind === "item" ? out[1].item.id : "", "wf");
});

test("a queued agent is work in hand, counted apart from the running ones", () => {
  const tasks = [task("a", { status: "queued" }), task("b"), task("c", { status: "queued" })];
  assert.ok(isLive(tasks[0]));
  assert.equal(countsLabel(counts(tasks)), "1 working · 2 queued");
});

test("the roster lists parents before their children with depth", () => {
  const items: import("convergence").TranscriptItem[] = [];
  applyTask(items, task("a"));
  applyTask(items, task("b", { parentTaskId: "a" }));
  applyTask(items, task("c"));
  assert.deepEqual(
    allTasks(items).map((r) => [r.task.id, r.depth]),
    [
      ["a", 0],
      ["b", 1],
      ["c", 0],
    ],
  );
});

test("counts and labels read like the t3code roster", () => {
  const tasks = [
    task("a"),
    task("b", { status: "waiting" }),
    task("c", { status: "completed" }),
    task("d", { status: "failed" }),
  ];
  assert.equal(countsLabel(counts(tasks)), "2 working · 1 completed · 1 failed");
  assert.equal(groupLabel(tasks), "Kicked off 4 subagents");
  assert.equal(groupLabel([task("c", { status: "completed" })]), "Ran 1 subagent");
});

test("elapsed time, tokens and the meta line", () => {
  const start = "2026-09-23T10:00:00Z";
  const now = Date.parse("2026-09-23T10:01:05Z");
  assert.equal(elapsed(task("a", { startedAt: start }), now), "1m 05s");
  assert.equal(elapsed(task("a", { startedAt: start, endedAt: "2026-09-23T10:00:42Z" }), now), "42s");
  assert.equal(elapsed(task("a"), now), null);
  assert.equal(tokens(850), "850 tok");
  assert.equal(tokens(12_345), "12k tok");
  assert.equal(tokens(1_500), "1.5k tok");
  const meta = metaLine(
    task("a", { model: "sonnet", effort: "high", startedAt: start, usage: { usedTokens: 1500 }, toolUses: 1 }),
    now,
  );
  assert.equal(meta, "sonnet · high · 1m 05s · 1.5k tok · 1 tool");
});

test("a finished subagent shows what it handed back, once", () => {
  const spawn = fixtureItem({
    id: "c1",
    role: "tool",
    call: { id: "c1", content: [{ type: "text", text: "Found 3 files." }] },
  });
  const item = fixtureItem({
    id: "task-x",
    role: "task",
    task: task("x", { status: "completed", toolCallId: "c1" }),
    items: [],
  });
  assert.equal(resultOf(item, [spawn, item]), "Found 3 files.");
  item.task.summary = "The summary";
  assert.equal(resultOf(item, [spawn, item]), "The summary");
  item.items?.push(fixtureItem({ id: "m", role: "assistant", text: "The summary" }));
  assert.equal(resultOf(item, [spawn, item]), null, "the last message already says it");
  item.task.status = "running";
  assert.equal(resultOf({ ...item, items: [] }, [spawn, item]), null, "nothing while it runs");
});

Versions

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <3105.1 KB5 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.