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

mentions.test.ts5.8 KB
// Regression coverage for mentions 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 {
  acceptMention,
  findTags,
  keepSegments,
  matchMentions,
  mentionInstructions,
  partOf,
  removeTag,
  tagTokens,
} from "./mentions.ts";
import { sendBlocks } from "./composer.ts";
import { harness } from "./ui-for-tests.test.ts";
import * as Effect from "effect/Effect";

const mention = (token: string, extra: Partial<import("convergence").Mention> = {}) => ({
  plugin: "subagents",
  id: token,
  token,
  label: token.split("/").join(" · "),
  prompt: `Delegate {token} with spawn_subagent: {part}`,
  ...extra,
});
const mentions = [
  mention("codex/gpt-5.5"),
  mention("codex/gpt-5.5/high", { keywords: "Codex GPT-5.5 High" }),
  mention("claude/haiku"),
];

test("a tag is a known token after @, between spaces", () => {
  const text = "@codex/gpt-5.5/high fix the parser, then @claude/haiku reviews @src/main.rs and me@codex/gpt-5.5";
  const tags = findTags(text, mentions);
  assert.deepEqual(
    tags.map((t) => [t.start, t.end, t.mention.token]),
    [
      [0, 19, "codex/gpt-5.5/high"],
      [41, 54, "claude/haiku"],
    ],
  );
  assert.deepEqual([...tagTokens(text, mentions)], ["@codex/gpt-5.5/high", "@claude/haiku"]);
  assert.deepEqual(findTags(text, []), [], "no mentions offered, no tags");
});

test("the picker matches tokens first, then names and keywords", () => {
  assert.deepEqual(
    matchMentions(mentions, "codex").map((m) => m.token),
    ["codex/gpt-5.5", "codex/gpt-5.5/high"],
  );
  assert.deepEqual(
    matchMentions(mentions, "haiku").map((m) => m.token),
    ["claude/haiku"],
  );
  assert.deepEqual(
    matchMentions(mentions, "High").map((m) => m.token),
    ["codex/gpt-5.5/high"],
  );
  assert.deepEqual(matchMentions(mentions, "src/ma"), []);
  assert.equal(matchMentions(mentions, "", 2).length, 2);
});

test("choosing a tag replaces what was typed after @", () => {
  assert.equal(acceptMention("please @cod", mentions[1]), "please @codex/gpt-5.5/high ");
});

test("deleting into a tag deletes the whole tag", () => {
  const text = "ask @codex/gpt-5.5/high about it";
  // Backspace at the end of the tag.
  assert.equal(keepSegments(text, "ask @codex/gpt-5.5/hig about it", mentions), "ask about it");
  // Delete at its start.
  assert.equal(keepSegments(text, "ask codex/gpt-5.5/high about it", mentions), "ask about it");
  // A selection across the tag and some text takes both.
  assert.equal(keepSegments(text, "ask @codex/gpt-5.5", mentions), "ask ");
  assert.equal(keepSegments(text, "ask @codex/gpt-5.5t", mentions), "ask t");
  // The space after it is text: deleting it keeps the tag.
  const tail = "ask @codex/gpt-5.5/high ";
  assert.equal(keepSegments(tail, "ask @codex/gpt-5.5/high", mentions), "ask @codex/gpt-5.5/high");
  // Deleting the whole tag, typing, or editing elsewhere is left alone.
  assert.equal(keepSegments(text, "ask  about it", mentions), "ask  about it");
  assert.equal(keepSegments(text, "ask @codex/gpt-5.5/high about its", mentions), "ask @codex/gpt-5.5/high about its");
  assert.equal(keepSegments(text, "ak @codex/gpt-5.5/high about it", mentions), "ak @codex/gpt-5.5/high about it");
  // A deletion that also types (autocorrect) is not a plain deletion.
  assert.equal(keepSegments(text, "ask @codex/gpt-5.5/X about it", mentions), "ask @codex/gpt-5.5/X about it");
});

test("a chip's x takes the tag out without doubling a space", () => {
  const text = "ask @claude/haiku about it";
  assert.equal(removeTag(text, findTags(text, mentions)[0]), "ask about it");
  const first = "@claude/haiku review this";
  assert.equal(removeTag(first, findTags(first, mentions)[0]), "review this");
});

test("each tag stands for the text after it, or before it when it ends the message", () => {
  const text = "@codex/gpt-5.5/high write the tests @claude/haiku review them";
  const tags = findTags(text, mentions);
  assert.equal(partOf(text, tags, 0), "write the tests");
  assert.equal(partOf(text, tags, 1), "review them");
  const trailing = "summarize the design doc @claude/haiku";
  assert.equal(partOf(trailing, findTags(trailing, mentions), 0), "summarize the design doc");
  const alone = "@claude/haiku";
  assert.equal(partOf(alone, findTags(alone, mentions), 0), "@claude/haiku");
});

test("the instructions fill each mention's prompt once", () => {
  const text = "@codex/gpt-5.5/high write the tests @claude/haiku review them";
  assert.equal(
    mentionInstructions(text, mentions),
    "Delegate @codex/gpt-5.5/high with spawn_subagent: write the tests\n\nDelegate @claude/haiku with spawn_subagent: review them",
  );
  assert.equal(mentionInstructions("no tags @src/a.rs", mentions), "");
});

// The chat plugin's own send path, run with a fake api: a tag is not a
// file, and what it asks follows the message.
test("the composer sends a tag's instruction and no file for it", async () => {
  const h = harness();
  h.mentions.push(...mentions);
  // This focused mentions fixture retains Alpha's idle send path; the
  // stateful scheduler fixtures exercise enqueue and admission separately.
  h.controller.scheduler.available = false;
  await Effect.runPromise(sendBlocks(h.controller, "c1", "@codex/gpt-5.5/high check @src/main.rs", []));
  assert.deepEqual(h.notices, [], "sending must not report an error");
  const sent = h.calls.filter((call) => call.method === "host/chats.send").map((call) => call.params);
  assert.deepEqual(sent, [
    {
      chatId: "c1",
      blocks: [
        { type: "text", text: "@codex/gpt-5.5/high check @src/main.rs" },
        { type: "file_ref", path: "src/main.rs" },
        { type: "text", text: "Delegate @codex/gpt-5.5/high with spawn_subagent: check @src/main.rs" },
      ],
    },
  ]);
});

assert.equal(typeof sendBlocks, "function", "the imported implementation is available");

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.