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

workflow-view.test.ts7.2 KB
// Regression coverage for workflow-view using the real chat modules.
// Fixtures exercise the same behavior and assertions as the JavaScript tests.
import { fixtureTask, taskItemAt, taskEvent } from "./ui-for-tests.test.ts";
import assert from "node:assert/strict";
import test from "node:test";
import * as Effect from "effect/Effect";
import { harness, nodes, tree, chat as makeChat, transcript as initial } from "./ui-for-tests.test.ts";
import { applyEvent } from "./transcript.ts";
import { renderWorkflow as workflowView } from "./views/tasks.ts";
import type { Chat } from "convergence";
import type { Transcript } from "./state.ts";
import type { TaskItem } from "./grouping.ts";
const h = harness();
const opened: string[] = [],
  stopped: string[] = [];
h.controller.state.agents.agent = { capabilities: { cancelTask: true } } as import("convergence").AgentInfo;
h.controller.state.composers.c1 = {
  ...h.controller.composer("c1"),
  options: [
    {
      id: "model",
      name: "Model",
      kind: "select",
      category: "model",
      value: "claude-haiku-4-5",
      choices: [{ value: "claude-haiku-4-5", name: "Haiku 4.5" }],
    },
  ],
};
h.controller.openSubagent = (_chat, id) =>
  Effect.sync(() => {
    opened.push(id);
  });
h.controller.openWorkflow = (_chat, id) =>
  Effect.sync(() => {
    opened.push("workflow:" + id);
  });
h.controller.stopTask = (_chat, id) =>
  Effect.sync(() => {
    stopped.push(id);
  });
const renderWorkflow = (chat: Chat, t: Transcript, item: TaskItem, pane?: boolean) =>
  tree(workflowView(h.v, chat, t, item, pane));
const chat = makeChat({ id: "c1" });
const transcript = initial;
const task = taskEvent;
const workflow = (
  status: import("convergence").TaskStatus,
  extra: { workflow?: Partial<import("convergence/protocol").Workflow>; task?: Parameters<typeof fixtureTask>[1] } = {},
) =>
  task({
    id: "wf",
    title: "Review the branch",
    name: "review",
    status,
    background: true,
    prompt: "export const meta = {}",
    workflow: { logs: [], phases: [{ title: "Find" }, { title: "Verify" }], ...extra.workflow },
    ...extra.task,
  });
const agent = (
  index: number,
  phase: string,
  status: import("convergence").TaskStatus,
  extra: Parameters<typeof fixtureTask>[1] = {},
) =>
  task({
    id: `wf/${index}`,
    title: `agent-${index}`,
    status,
    phase,
    parentTaskId: "wf",
    model: "claude-haiku-4-5-20251001",
    ...extra,
  });

const texts = (node: unknown) =>
  nodes(node)
    .filter((n) => n.type === "text")
    .map((n) => n.text);
const buttons = (node: unknown) => nodes(node).filter((n) => n.type === "button");

const running = () => {
  const t = transcript();
  applyEvent(t, workflow("running"));
  applyEvent(t, agent(1, "Find", "completed", { summary: "3 findings", usage: { usedTokens: 12_000 } }));
  applyEvent(t, agent(2, "Find", "running", { activity: "Grep TODO" }));
  applyEvent(t, agent(3, "Find", "queued"));
  return t;
};

test("a running workflow opens on its phases, with a segment per agent", () => {
  const t = running();
  assert.equal(t.items.length, 1, "its agents live inside it");
  const card = renderWorkflow(chat, t, taskItemAt(t.items, 0));
  assert.equal(card.id, "workflow-wf");
  const [header, body, footer] = card.children;
  assert.ok(
    texts(header).some((p) => p === "Review the branch"),
    "the title",
  );
  assert.ok(
    texts(header).some((p) => p === "review"),
    "the script's name",
  );
  const segments = nodes(header).filter((n) => n.type === "div" && n.borderRadius === "full");
  assert.deepEqual(
    segments.map((s) => s.background),
    ["success", "primary", "muted", "muted"],
    "3 agents and the phase not reached",
  );
  assert.ok(body, "open while it runs");
  const labels = texts(body);
  assert.ok(labels.includes("Find") && labels.includes("Verify"));
  assert.ok(labels.includes("agent-2"), "the running phase shows its agents");
  assert.ok(labels.includes("Haiku 4.5"), "models by their names");
  assert.ok(labels.includes("Grep TODO") && labels.includes("Waiting for a slot"));
  assert.ok(labels.includes("Not started"), "the phase not reached");
  assert.ok(buttons(footer).some((b) => b.label === "Stop workflow"));
});

test("an agent row opens that agent, and the phase row folds its agents", async () => {
  const t = running();
  const body = renderWorkflow(chat, t, taskItemAt(t.items, 0)).children[1];
  const row = nodes(body).find(
    (n) => n.type === "div" && typeof n.onClick === "function" && texts(n).includes("agent-2"),
  );
  row?.onClick();
  await h.settle();
  assert.deepEqual(opened.splice(0), ["wf/2"]);
  const phase = nodes(body).find(
    (n) =>
      n.type === "div" && typeof n.onClick === "function" && texts(n).includes("Find") && !texts(n).includes("agent-1"),
  );
  assert.ok(phase);
  phase.onClick();
  const folded = renderWorkflow(chat, t, taskItemAt(t.items, 0)).children[1];
  assert.ok(!texts(folded).includes("agent-2"), "the user closed the phase");
});

test("a run seen finishing closes, shows its result and plays its burst once", () => {
  const t = running();
  applyEvent(t, agent(2, "Find", "completed"));
  applyEvent(t, agent(3, "Find", "completed"));
  applyEvent(
    t,
    workflow("completed", { workflow: { result: { confirmed: 2 } }, task: { summary: '{"confirmed":2}' } }),
  );
  const card = renderWorkflow(chat, t, taskItemAt(t.items, 0));
  assert.equal(card.children.length, 1, "a finished run starts closed");
  assert.ok(
    texts(card.children[0]).some((p) => p === '· {"confirmed":2}'),
    "its result on the header",
  );
  assert.equal(card.celebrate.token, "workflow-wf");
  card.celebrate.onDone();
  assert.equal(renderWorkflow(chat, t, taskItemAt(t.items, 0)).celebrate, undefined);

  card.children[0].onClick();
  const open = renderWorkflow(chat, t, taskItemAt(t.items, 0));
  const code = nodes(open.children[1]).find((n) => n.type === "code");
  assert.ok(code);
  assert.match(code.text, /"confirmed": 2/);
  assert.ok(!buttons(open.children[2]).some((b) => b.label === "Stop workflow"), "nothing to stop");
});

test("a run read back from history plays no burst", () => {
  const t = transcript();
  applyEvent(t, workflow("completed"));
  assert.equal(renderWorkflow(chat, t, taskItemAt(t.items, 0)).celebrate, undefined);
});

test("an agent that needs the user makes the run need the user", () => {
  const t = running();
  applyEvent(t, agent(2, "Find", "waiting"));
  const header = renderWorkflow(chat, t, taskItemAt(t.items, 0)).children[0];
  assert.ok(texts(header).some((p) => p === "Needs you"));
});

test("the pane shows everything open and offers the script", () => {
  const t = running();
  applyEvent(t, workflow("completed"));
  const pane = renderWorkflow(chat, t, taskItemAt(t.items, 0), true);
  assert.equal(pane.background, undefined, "the pane is the surface");
  assert.ok(texts(pane.children[1]).includes("agent-1"), "done phases are open too");
  const script = buttons(pane.children[2]).find((b) => b.label === "Script");
  assert.ok(!buttons(pane.children[2]).some((b) => b.label === "Open"), "it is already open");
  assert.ok(script);
  script.onClick();
  const shown = renderWorkflow(chat, t, taskItemAt(t.items, 0), true);
  assert.ok(nodes(shown.children[1]).some((n) => n.type === "code" && n.language === "javascript"));
});

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.