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

grouping.ts3.5 KB
// Consecutive tools and reasoning form groups without changing transcript order.
// Pure display projection: stored transcript items keep their original order.
import type { TranscriptItem, ToolKind } from "convergence";
export type TaskItem = Extract<TranscriptItem, { role: "task" }>;
export type ToolGroup = { kind: "tools"; id: string; items: TranscriptItem[] };
export type TaskGroup = { kind: "tasks"; id: string; items: TaskItem[] };
export type FoldEntry = { kind: "item"; item: TranscriptItem } | ToolGroup | TaskGroup;

const LABELS = {
  read: ["Read", "Reads"],
  search: ["Search", "Searches"],
  execute: ["Run", "Runs"],
  edit: ["Edit", "Edits"],
  delete: ["Delete", "Deletes"],
  move: ["Move", "Moves"],
  fetch: ["Fetch", "Fetches"],
  think: ["Thought", "Thoughts"],
  task: ["Task", "Tasks"],
  switch_mode: ["Mode change", "Mode changes"],
  other: ["Other tool", "Other tools"],
};

// Every tool kind and reasoning can share a group. Messages, compaction,
// and other standalone entries end it so nothing moves across them.
export function foldItems(items: readonly TranscriptItem[]): FoldEntry[] {
  const out: FoldEntry[] = [];
  let group: ToolGroup | null = null;
  for (const item of items) {
    if (item.role !== "tool" && item.role !== "reasoning") {
      group = null;
      out.push({ kind: "item", item });
      continue;
    }
    if (!group) {
      group = { kind: "tools", id: `tools-${item.id}`, items: [] };
      out.push(group);
    }
    group.items.push(item);
  }
  return out;
}

// "3 Reads, 1 Search": the calls in a fold by kind, in order of first use.
export function groupTitle(items: readonly TranscriptItem[]) {
  const counts = new Map<ToolKind, number>();
  for (const item of items) {
    if (item.role !== "tool") continue;
    counts.set(item.call.kind ?? "other", (counts.get(item.call.kind ?? "other") ?? 0) + 1);
  }
  const parts = [...counts].sort(([a], [b]) => Number(a === "other") - Number(b === "other"));
  return parts.map(([kind, n]) => `${n} ${(LABELS[kind] ?? LABELS.other)[n === 1 ? 0 : 1]}`).join(", ") || "Reasoning";
}

// Lines added and removed across the diffs of a fold, or null when no call
// carried a diff.
export function editStats(items: readonly TranscriptItem[]) {
  let added = 0;
  let removed = 0;
  let any = false;
  for (const item of items) {
    if (item.role !== "tool") continue;
    for (const c of item.call.content ?? []) {
      if (c.type !== "diff") continue;
      any = true;
      const stats = c.diff ? countPatch(c.diff) : countTexts(c.oldText ?? "", c.newText ?? "");
      added += stats.added;
      removed += stats.removed;
    }
  }
  return any ? { added, removed } : null;
}

function countPatch(diff: string) {
  let added = 0;
  let removed = 0;
  for (const line of diff.split("\n")) {
    if (line.startsWith("+++") || line.startsWith("---")) continue;
    if (line.startsWith("+")) added++;
    else if (line.startsWith("-")) removed++;
  }
  return { added, removed };
}

// Without a patch, count the lines of one text the other does not have.
function countTexts(oldText: string, newText: string) {
  const seen = new Map();
  for (const line of lines(oldText)) seen.set(line, (seen.get(line) ?? 0) + 1);
  let added = 0;
  for (const line of lines(newText)) {
    const left = seen.get(line) ?? 0;
    if (left > 0) seen.set(line, left - 1);
    else added++;
  }
  let removed = 0;
  for (const left of seen.values()) removed += left;
  return { added, removed };
}

function lines(text: string) {
  return text === "" ? [] : text.replace(/\n$/, "").split("\n");
}

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.