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/panes.ts4 KB
// Panes rendering preserves the chat's retained tree and interaction keys.
import { html } from "convergence";
import { VISIBLE, reconcile as reconcileCycle, visible } from "../switcher.ts";
import type { Element } from "convergence";
import type { ChatView } from "../controller.ts";
import { renderTranscript, reading } from "./rows.ts";
import { renderComposer, renderLanding } from "./composer.ts";
import { column, row, fill, full } from "./styles.ts";
export function switcherRow(v: ChatView, id: string, chosen: boolean): Element | null {
const chat = v.controller.state.chats[id];
if (!chat) return null;
const workspace = v.controller.state.workspaces[chat.workspaceId]?.name ?? "";
return html`
<item id=${`switch-${id}`} label=${v.controller.titleOf(chat)} sublabel=${workspace || undefined} selected=${chosen}
busy=${v.controller.isRunning(chat)} icon="bot" iconSvg=${v.controller.state.agents[chat.agentId]?.icon}
onClick=${() => {
v.controller.state.switcher = { list: [id], selected: id };
v.ctx.run(v.controller.commitSwitcher());
}} />`;
}
export function renderSwitcher(v: ChatView): Element | null {
// The list is checked against the chats the app still has: one closed
// while the cycle is open must not leave a row behind.
const open = reconcileCycle(v.controller.state.switcher, v.controller.state.chats);
v.controller.state.switcher = open;
const { rows } = open ? visible(open.list, open.selected, VISIBLE) : { rows: [] };
const keys = {
up: () => v.controller.switchBy(-1),
left: () => v.controller.switchBy(-1),
down: () => v.controller.switchBy(1),
right: () => v.controller.switchBy(1),
enter: () => v.ctx.run(v.controller.commitSwitcher()),
escape: v.controller.closeSwitcher,
};
// The overlay draws the card around the column; only the rows are ours.
return html`
<overlay id="chat-switcher" open=${!!open} width=${544} hold="ctrl" onRelease=${() => v.ctx.run(v.controller.commitSwitcher())} onDismiss=${v.controller.closeSwitcher} keys=${keys}>
<div ...${column} gap=${2} padding="small">
<div ...${row} gap="small" padding="small" paddingX=${8} paddingY=${2}>
<text style="section" size=${9.84} weight=${700} tone="muted">Recent chats</text>
</div>
${rows.map((id) => switcherRow(v, id, id === open?.selected))}
</div>
</overlay>`;
}
// One chat: the landing page while it is fresh, else its transcript with
// the composer under it.
export function renderPane(v: ChatView, chatId: string): Element | null {
const chat = v.controller.state.chats[chatId];
if (!chat) return null;
v.ctx.run(v.controller.ensureChat(chat.id));
const t = v.controller.transcript(chat.id);
const landing = t.loaded && !t.items.length && !t.running;
const body = !t.loaded
? html`<empty>Loading…</empty>`
: landing
? renderLanding(v, chat)
: html`
<div ...${column} ...${fill} ...${full} padding="small">
${renderTranscript(v, chat)}
<div ...${column} gap="small" padding="small" paddingY="medium">${reading(v, [renderComposer(v, chat)])}</div>
</div>`;
// The landing page's stage runs to the pane's edges; the transcript keeps
// the pane's inset.
return html`
<div ...${column} ...${fill} ...${full} id=${`pane-${chat.id}`} padding=${landing ? undefined : "small"} position="relative">
${body}
</div>`;
}
// The shown chat's pane view, made the first time it shows.
export function renderCenter(v: ChatView): Element | null {
const app = v.ctx.app;
if (app.route !== "chat") return null;
if (!app.chatId) return html`<empty>Select a chat or press ⌘N to start one.</empty>`;
const chat = v.controller.state.chats[app.chatId];
if (!chat) {
v.ctx.run(v.controller.loadChat(app.chatId));
return html`<empty>Loading…</empty>`;
}
v.ctx.run(v.controller.followApp());
const pane = v.controller.state.panes[chat.id];
return pane ? html`<view view=${pane.view.id} />` : html`<empty>Loading…</empty>`;
}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.