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

transcript.ts5 KB
// Apply agent snapshots and deltas in place without work beyond the affected transcript.
import type { AgentEventKind, TranscriptItem } from "convergence";
import type { Transcript } from "./state.ts";
import { applyTask, findTask, isLive } from "./subagents.ts";
export function findItem(t: { items: TranscriptItem[] }, id: string | undefined) {
  for (let i = t.items.length - 1; i >= 0; i--) if (t.items[i]?.id === id) return t.items[i] ?? null;
  return null;
}
export function applyText(
  t: { items: TranscriptItem[] },
  kind: Extract<AgentEventKind, { event: "text_delta" | "reasoning_delta" }>,
  role: "assistant" | "reasoning",
) {
  const existing = findItem(t, kind.itemId);
  if (existing && (existing.role === "assistant" || existing.role === "reasoning"))
    existing.text = kind.mode === "replace" ? kind.text : existing.text + kind.text;
  else t.items.push({ id: kind.itemId, role, text: kind.text });
}
export function applyEvent(t: Transcript, kind: AgentEventKind, taskId?: string): boolean {
  if (kind.event === "task") {
    const { event, ...task } = kind;
    const before = task.workflow ? findTask(t.items, task.id)?.task.status : null;
    applyTask(t.items, task);
    // A workflow seen finishing gets its burst, once. One read back from
    // history has no `before` and stays quiet.
    if (before && isLive({ status: before }) && task.status === "completed") (t.finished ??= new Set()).add(task.id);
    return true;
  }
  if (taskId) {
    if (kind.event === "approval" || kind.event === "question") kind.taskId ??= taskId;
    if (kind.event === "plan" || kind.event === "run_started" || kind.event === "run_finished") return false;
  }
  // The list the event's items go into.
  const task = taskId ? findTask(t.items, taskId) : null;
  const scope = task ? { items: (task.items ??= []) } : t;
  switch (kind.event) {
    case "text_delta":
      applyText(scope, kind, "assistant");
      return true;
    case "reasoning_delta":
      applyText(scope, kind, "reasoning");
      return true;
    case "tool_call_started": {
      const { event, ...call } = kind;
      call.content ??= [];
      call.locations ??= [];
      const existing = findItem(scope, call.id);
      if (existing?.role === "tool") existing.call = call;
      else scope.items.push({ id: call.id, role: "tool", call });
      return true;
    }
    case "tool_call_updated": {
      const item = findItem(scope, kind.id);
      if (item?.role !== "tool") return false;
      const call = item.call;
      if (kind.status !== undefined) call.status = kind.status;
      if (kind.title !== undefined) call.title = kind.title;
      if (kind.kind !== undefined) call.kind = kind.kind;
      if (kind.input !== undefined) call.input = kind.input;
      // Some agents (Cline) send a result only here.
      if (kind.output !== undefined) call.output = kind.output;
      if (kind.content !== undefined) call.content = kind.content;
      if (kind.locations !== undefined) call.locations = kind.locations;
      call.content ??= [];
      call.locations ??= [];
      if (kind.outputDelta) {
        const last = call.content[call.content.length - 1];
        if (last?.type === "text") last.text += kind.outputDelta;
        else if (last?.type === "terminal") last.output += kind.outputDelta;
        else call.content.push({ type: "text", text: kind.outputDelta });
      }
      return true;
    }
    // Kept only to place the kernel's card and to know the chat waits.
    case "approval": {
      const { event, ...req } = kind;
      t.approvals = [...t.approvals.filter((a) => a.id !== req.id), req];
      return true;
    }
    case "question": {
      const { event, ...req } = kind;
      t.questions = [...t.questions.filter((q) => q.id !== req.id), req];
      return true;
    }
    // Answered in another client of the same agent, or withdrawn.
    case "approval_resolved": {
      const before = t.approvals.length;
      t.approvals = t.approvals.filter((a) => a.id !== kind.id);
      return t.approvals.length !== before;
    }
    case "question_resolved": {
      const before = t.questions.length;
      t.questions = t.questions.filter((q) => q.id !== kind.id);
      return t.questions.length !== before;
    }
    case "plan":
      t.plan = { entries: kind.entries };
      return true;
    // The agent went on by itself, for example after a background task.
    case "run_started":
      t.running = true;
      return true;
    case "run_finished":
      t.running = false;
      t.approvals = [];
      t.questions = t.questions.filter((q) => q.responseMode === "message");
      if (kind.outcome.status === "cancelled")
        t.items.push({ id: `n-${Date.now()}`, role: "notice", text: "Stopped." });
      if (kind.outcome.status === "failed")
        t.items.push({ id: `n-${Date.now()}`, role: "notice", text: kind.outcome.message });
      return true;
    case "notice":
      if (kind.level === "error") {
        t.items.push({ id: `n-${Date.now()}`, role: "notice", text: kind.message });
        return true;
      }
      return false;
    default:
      return false;
  }
}

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.