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

  • Control chats chats.controlHighCreates chats, sends prompts and cancels runs.Add and remove workspaces, and create, rename, check off and archive chats
  • Read chats chats.readMediumReads your transcripts and chat lists.List workspaces and chats, and follow them as agents work
  • Show panels ui.slotsLowShows views in the listed parts of the window.Draw the sidebarSlots: left

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

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <320.2 KB3 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.