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
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
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 105.1 KB | 5 permissions | Listed |
No comments yet.