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

models.test.ts5.3 KB
// Regression coverage for models using the real chat modules.
// Fixtures exercise the same behavior and assertions as the JavaScript tests.
import assert from "node:assert/strict";
import test from "node:test";
import * as Effect from "effect/Effect";
import type { Chat, ConfigOption } from "convergence";
import { sections, createModelPicker } from "./models.ts";
import { harness, tree as inspect, nodes, chat as makeChat } from "./ui-for-tests.test.ts";
assert.ok(typeof sections === "function");
const choices = [
  { value: "a/fast", name: "Fast", group: "Provider A" },
  { value: "a/deep", name: "Deep", group: "Provider A" },
  { value: "b/deep", name: "Deep", group: "Provider B" },
  { value: "local", name: "Local" },
];

test("favorites are unique, provider-qualified, and leave catalog order intact", () => {
  const result = sections(choices, ["b/deep", "removed"], "");
  assert.deepEqual(
    result.map((group) => [group.name, group.choices.map((choice) => choice.value)]),
    [
      ["Favorites", ["b/deep"]],
      ["Provider A", ["a/fast", "a/deep"]],
      ["", ["local"]],
    ],
  );
});

test("search matches names, ids, and providers together, ignoring case and whitespace", () => {
  assert.deepEqual(sections(choices, [], "  DEEP provider B ")[0].choices, [choices[2]]);
  assert.deepEqual(sections(choices, [], "a/fast")[0].choices, [choices[0]]);
  assert.deepEqual(sections(choices, [], "missing"), []);
  assert.equal(sections(choices, [], "Local")[0].name, "");
});

test("ungrouped models stay flat, with only favorites getting a heading", () => {
  const flat = [
    { value: "default", name: "Default", group: undefined },
    { value: "fast", name: "Fast" },
  ];
  assert.deepEqual(sections(flat, [], ""), [{ name: "", choices: flat }]);
  assert.deepEqual(sections(flat, ["fast"], ""), [
    { name: "Favorites", favorite: true, choices: [flat[1]] },
    { name: "", choices: [flat[0]] },
  ]);
});

test("the settings menu lists each setting with its value, the models grouped with favorites first", async () => {
  const h = harness(),
    selections: string[][] = [];
  h.controller.setOption = (...args) =>
    Effect.sync(() => {
      selections.push(args);
    });
  const chat = makeChat({ id: "chat", agentId: "one" });
  const model: ConfigOption = { id: "model", name: "Model", kind: "select", category: "model", value: "a/fast", choices };
  const effort: ConfigOption = {
    id: "effort",
    name: "Effort",
    kind: "select",
    category: "reasoning",
    value: "high",
    choices: [
      { value: "low", name: "Low", description: "Quick answers" },
      { value: "high", name: "High" },
    ],
  };
  const speed: ConfigOption = {
    id: "speed",
    name: "Speed",
    kind: "select",
    category: "other",
    value: "standard",
    choices: [{ value: "standard", name: "Standard" }],
  };
  const open = async () => {
    const picker = createModelPicker(h.controller);
    await Effect.runPromise(picker.load);
    return (chat: Chat) => inspect(picker.render(h.v, chat, { ...model, choices: model.choices?.map((c) => ({ ...c, description: "About it" })) }, [effort, speed]));
  };
  type Menu = {
    type: string;
    label: string;
    detail: string;
    options: unknown;
    onStar: (e: { value: string; option?: string }) => void;
    onChange: (e: { value: string; option?: string }) => void;
  };
  let menu = (await open())(chat) as unknown as Menu;
  assert.equal(menu.type, "combobox");
  // The trigger names the model and, fainter, its effort only.
  assert.equal(menu.label, "Fast");
  assert.equal(menu.detail, "High");
  const rows = menu.options as { value: string; label: string; detail: string; submenu: { value: string; options: { value: string; group?: string; starred?: boolean; description?: string }[] } }[];
  assert.deepEqual(
    rows.map((row) => [row.label, row.detail, row.submenu.value]),
    [
      ["Model", "Fast", "a/fast"],
      ["Effort", "High", "high"],
      ["Speed", "Standard", "standard"],
    ],
  );
  assert.deepEqual(
    rows[0].submenu.options.map((o) => [o.value, o.group, o.starred]),
    [
      ["a/fast", "Provider A", false],
      ["a/deep", "Provider A", false],
      ["b/deep", "Provider B", false],
      ["local", undefined, false],
    ],
  );
  // Choices are names only: their descriptions only took room.
  assert.ok(rows.every((row) => row.submenu.options.every((o) => o.description === undefined)));

  // A star saves the favorite without choosing the model; the starred one
  // moves to its own group on top.
  menu.onStar({ value: "b/deep", option: "model" });
  await new Promise(setImmediate);
  assert.equal(selections.length, 0);
  assert.deepEqual(h.stored.modelFavorites, { one: ["b/deep"] });
  menu = (await open())(chat) as unknown as Menu;
  const models = (menu.options as typeof rows)[0].submenu.options;
  assert.deepEqual([models[0].value, models[0].group, models[0].starred], ["b/deep", "Favorites", true]);
  // Favorites are the agent's own.
  const other = (await open())({ ...chat, agentId: "two" });
  assert.ok((other.options as typeof rows)[0].submenu.options.every((o) => !o.starred));

  // A choice in a submenu sets that setting and gives the prompt the keyboard.
  menu.onChange({ value: "low", option: "effort" });
  await h.settle();
  assert.deepEqual(selections, [["chat", "effort", "low"]]);
  assert.ok(h.focused.at(-1)?.startsWith("prompt-"));
});

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.