Official
sidebar
Built-in sidebar 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/sidebar@0.2.0
Permissions in 0.2.0
Files
preview.ts2.7 KB
// The live preview line of a chat row: the tail of the agent's current
// message or tool title, kept from streamed events without a transcript.
import type { AgentEventKind, TranscriptItem } from "convergence";
export interface Preview {
line: string;
textId: string | null;
text: string;
toolId: string | null;
tool: string;
latest: "text" | "tool" | null;
revision: number;
loading: boolean;
}
export const createPreview = (): Preview => ({
line: "",
textId: null,
text: "",
toolId: null,
tool: "",
latest: null,
revision: 0,
loading: false,
});
const lastLine = (text: string): string =>
text
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.at(-1) ?? "";
// The row draws a message's line as plain text, so its Markdown marks
// would show as characters: a list or heading mark, a link's brackets,
// bold and code. A tool title is a command and keeps them.
const plain = (line: string): string =>
line
.replace(/^(?:#{1,6}\s+|>\s*|[-*+]\s+(?:\[[ xX]\]\s+)?|\d+[.)]\s+)/, "")
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
.replace(/\*\*|`/g, "");
// Keep only the current text tail and tool title, never a second transcript.
// A late tool completion must not replace a newer assistant message.
export function updatePreview(preview: Preview, event: AgentEventKind): boolean {
const before = preview.line;
if (event.event === "text_delta" || event.event === "reasoning_delta") {
preview.text = (
(event.mode === "replace" || preview.textId !== event.itemId ? "" : preview.text) + event.text
).slice(-512);
preview.textId = event.itemId;
const line = plain(lastLine(preview.text));
if (line) {
preview.line = line;
preview.latest = "text";
}
} else if (event.event === "tool_call_started") {
preview.toolId = event.id;
preview.tool = lastLine(event.title || event.name || "Tool call");
preview.line = preview.tool;
preview.latest = "tool";
} else if (event.event === "tool_call_updated" && event.id === preview.toolId && event.title) {
preview.tool = lastLine(event.title);
if (preview.latest === "tool") preview.line = preview.tool;
} else {
return false;
}
preview.revision++;
return before !== preview.line;
}
export function transcriptPreview(items: readonly TranscriptItem[]): Preview {
const preview = createPreview();
for (const item of items) {
if (item.role === "assistant" || item.role === "reasoning") {
updatePreview(preview, { event: "text_delta", itemId: item.id, text: item.text, mode: "replace" });
} else if (item.role === "tool") {
updatePreview(preview, { ...item.call, event: "tool_call_started" });
}
}
return preview;
}Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 20.2 KB | 3 permissions | Listed |
No comments yet.