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

options.test.ts3.1 KB
// A chat's settings belong to its agent: switching agents never leaves the
// last agent's model in the picker, even when the new one's fail to load,
// and a draft whose agent is gone says so.
import assert from "node:assert/strict";
import test from "node:test";
import * as Effect from "effect/Effect";
import { HostCallFailed } from "convergence/effect";
import type { AgentInfo } from "convergence";
import { loadOptions } from "./data.ts";
import { renderComposer } from "./views/composer.ts";
import { harness, chat, nodes } from "./ui-for-tests.test.ts";

test("a new agent's failed options leave none, not the last agent's", async () => {
  const h = harness((method) =>
    method === "host/chats.options" ? Effect.fail(new HostCallFailed({ method, code: -32603, message: "no catalog" })) : undefined,
  );
  h.controller.state.chats.c = chat({ id: "c", agentId: "codex" });
  const composer = h.controller.composer("c");
  composer.agentId = "codex";
  composer.options = [
    { id: "model", name: "Model", kind: "select", category: "model", value: "gpt", choices: [{ value: "gpt", name: "GPT" }] },
  ];
  h.controller.state.chats.c = chat({ id: "c", agentId: "opencode" });
  await Effect.runPromise(loadOptions(h.controller, "c"));
  assert.deepEqual(composer.options, []);
  assert.equal(composer.agentId, "opencode");
  assert.ok(h.notices.some((notice) => notice.includes("no catalog")), "the failure is said");
});

test("a draft whose agent was turned off asks for another", () => {
  const empty: Record<string, unknown> = {
    "host/chats.options": { options: [] },
    "host/chats.commands": { commands: [] },
    "host/chats.skills": { skills: [] },
  };
  const h = harness((method) => (method in empty ? Effect.succeed(empty[method]) : undefined));
  const draft = chat({ id: "d", agentId: "gone" });
  h.controller.state.chats.d = draft;
  h.controller.state.agentList = [{ id: "codex", name: "Codex", capabilities: {} } as AgentInfo];
  const picker = nodes(renderComposer(h.v, draft)).find((node) => node.id === "agent-d");
  assert.equal(picker?.placeholder, "Choose an agent");
});

test("an agent's options that arrive after a switch to another are dropped", async () => {
  let release!: () => void;
  const late = new Promise<void>((resolve) => (release = resolve));
  const h = harness((method, params) =>
    method === "host/chats.options" && (params as { chatId: string }).chatId === "c"
      ? Effect.promise(() => late).pipe(
          Effect.as({ options: [{ id: "model", name: "Model", kind: "select", category: "model", value: "old", choices: [] }] }),
        )
      : undefined,
  );
  h.controller.state.chats.c = chat({ id: "c", agentId: "codex" });
  const first = Effect.runPromise(loadOptions(h.controller, "c"));
  // The user picks another agent before Codex answered.
  h.controller.state.chats.c = chat({ id: "c", agentId: "opencode" });
  h.controller.composer("c").agentId = "opencode";
  h.controller.composer("c").loading = true;
  release();
  await first;
  assert.deepEqual(h.controller.composer("c").options, [], "Codex's model is not OpenCode's");
  assert.equal(h.controller.composer("c").loading, true, "OpenCode's own read is still on its way");
});

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.