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