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
views/title.ts3.4 KB
// Title rendering preserves the chat's retained tree and interaction keys.
import * as Effect from "effect/Effect";
import { renderSwitcher } from "./panes.ts";
import { workspacePicker } from "./composer.ts";
import { html } from "convergence";
import { allTasks, isLive } from "../subagents.ts";
import type { Element, Chat } from "convergence";
import type { ChatView } from "../controller.ts";
import { row, fill, titleChipAnchor } from "./styles.ts";
export function agentsButton(v: ChatView, chat: Chat): Element | null {
const t = v.controller.state.transcripts[chat.id];
const tasks = t ? allTasks(t.items).map((r) => r.task) : [];
if (!tasks.length) return null;
const working = tasks.filter(isLive).length;
return html`
<button id=${`agents-${chat.id}`} icon="network" label=${working ? `${working} working` : `${tasks.length}`} variant="ghost"
tooltip="Subagents of this chat" onClick=${() => v.ctx.run(v.controller.openAgents(chat))} />`;
}
export function renderTitle(v: ChatView): Element | null {
// The cycle still draws off the chat route: committing to a chat
// brings the route back with it, and an open cycle must never be
// left with no way to close it.
if (v.ctx.app.route !== "chat") return v.controller.state.switcher ? renderSwitcher(v) : null;
const chat = v.ctx.app.chatId ? v.controller.state.chats[v.ctx.app.chatId] : undefined;
const agent = chat ? v.controller.state.agents[chat.agentId] : null;
const can = agent?.capabilities;
const compact = () => {
if (chat)
v.ctx.run(
v.controller.host
.call("host/chats.compact", { chatId: chat.id })
.pipe(Effect.catch((error) => v.controller.notify.error(`Compact failed: ${error.message}`))),
);
};
const fork = () => {
if (chat)
v.ctx.run(
v.controller.host.call("host/chats.fork", { chatId: chat.id }).pipe(
Effect.flatMap((copy) => v.controller.app.select(copy.id, copy.workspaceId)),
Effect.catch((error) => v.controller.notify.error(`Fork failed: ${error.message}`)),
),
);
};
// The switcher goes over the window, not in the strip: the overlay
// takes no space here.
return html`
<div ...${row} ...${fill} gap="small" padding="small">
<button id="title-new-chat" icon="plus" variant="ghost" tooltip="New chat (⌘N)" onClick=${() => v.ctx.run(v.controller.kernel.call("run_command", { id: "sidebar.newChat" }))} />
<text style="heading" truncate text=${chat ? v.controller.titleOf(chat) : "Chat"} />
${chat && v.controller.isRunning(chat) ? html`<text style="body" size="sm" tone="muted">Running…</text>` : null}
${chat?.status === "failed" ? html`<text style="body" size="sm" tone="danger">Failed</text>` : null}
<spacer />
${chat ? agentsButton(v, chat) : null}
${
chat && can?.compact
? html`<button id=${`compact-${chat.id}`} icon="fold-vertical" variant="ghost" tooltip="Summarize the conversation to free context" onClick=${compact} />`
: null
}
${
chat && can?.fork
? html`<button id=${`fork-${chat.id}`} icon="git-branch" variant="ghost" tooltip="Copy this chat and continue separately" onClick=${fork} />`
: null
}
${chat ? workspacePicker(v, chat, "title", titleChipAnchor) : null}
${renderSwitcher(v)}
</div>`;
}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.