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

  • Control chats chats.controlHighCreates chats, sends prompts and cancels runs.Send prompts, answer agent questions, and create, change, fork, revert, stop and group chats
  • Run other plugins' commands commands.runHighRuns commands of other plugins.Run the sidebar's New chat command from the title bar
  • Read chats chats.readMediumReads your transcripts and chat lists.Show workspaces, tasks, chats and transcripts, and follow them as agents work
  • Read files fs.readMediumReads files in the listed places.Suggest workspace files for @ mentionsPlaces: the open workspace
  • Show panels ui.slotsLowShows views in the listed parts of the window.Draw the chat grid and the title barSlots: centertitle

Files

panes.ts5.4 KB
// Each chat has one pane view, kept while it is one of the last few
// shown so switching back is instant. Detail views (a subagent, a
// workflow, the agents list) open in tabs through the convergence/tabs
// service.
import * as Scope from "effect/Scope";
import * as Effect from "effect/Effect";
import type { Chat, Element, ViewHandle } from "convergence";
import type { Controller, ChatError, ChatView } from "./controller.ts";
import { renderPane } from "./views/panes.ts";
import { renderAgentsPane, renderSubagentPane, renderWorkflowPane } from "./views/tasks.ts";
import { findTask, nameOf } from "./subagents.ts";
import { loadOptions, loadTranscript, loadRequests, noteRecent, refreshHistory } from "./data.ts";

/** Chats whose panes stay mounted (the one shown and the last before it). */
const RETAINED = 3;

export const ensureComposer = Effect.fn("Chat.ensureComposer")(function* (c: Controller, chatId: string) {
  if (!c.state.composers[chatId]) {
    c.composer(chatId);
    if (c.state.restored) delete c.state.restored[chatId];
    yield* Effect.forkIn(loadOptions(c, chatId), c.scope, { startImmediately: true });
  }
});
export const ensureChat = Effect.fn("Chat.ensureChat")(function* (c: Controller, chatId: string) {
  if (!c.state.transcripts[chatId]) {
    c.transcript(chatId);
    yield* Effect.forkIn(loadTranscript(c, chatId), c.scope, { startImmediately: true });
    yield* Effect.forkIn(loadRequests(c, chatId), c.scope, { startImmediately: true });
  }
});
// The chat's pane view, made the first time it shows; the oldest beyond
// the retained few goes.
export const ensurePane = Effect.fn("Chat.ensurePane")(function* (c: Controller, chatId: string) {
  if (!c.state.panes[chatId]) {
    const view = yield* c.views
      .slot("pane", (ctx) => renderPane({ controller: c, ctx }, chatId), {
        key: `chat:${chatId}`,
        on: ["chat_changed", "agents_changed"],
      })
      .pipe(Effect.provideService(Scope.Scope, c.scope));
    c.state.panes[chatId] = { chatId, view };
  }
  c.state.recentPanes = [chatId, ...c.state.recentPanes.filter((id) => id !== chatId)];
  for (const old of c.state.recentPanes.splice(RETAINED)) removePane(c, old);
});
export function removePane(c: Controller, chatId: string) {
  c.state.panes[chatId]?.view.remove();
  delete c.state.panes[chatId];
  c.state.recentPanes = c.state.recentPanes.filter((id) => id !== chatId);
}
export const followApp = Effect.fn("Chat.followApp")(function* (c: Controller): Effect.fn.Return<void, ChatError> {
  const app = c.api.app.state();
  if (app.route !== "chat" || !app.chatId) return;
  const chat = c.state.chats[app.chatId];
  if (!chat) return;
  yield* Effect.forkIn(noteRecent(c, chat.id), c.scope, { startImmediately: true });
  if (chat.id !== c.state.promptedChat && !c.state.switcher) {
    c.state.promptedChat = chat.id;
    yield* c.views.focus(`prompt-${chat.id}`);
  }
  if (chat.id !== c.state.historyChat) {
    c.state.historyChat = chat.id;
    yield* Effect.forkIn(refreshHistory(c, chat.id), c.scope, { startImmediately: true });
  }
  yield* ensurePane(c, chat.id);
});
const paneView = Effect.fn("Chat.paneView")(function* (
  c: Controller,
  key: string,
  chatId: string,
  render: (v: ChatView) => Element | null,
) {
  let entry = c.state.paneViews[key];
  if (!entry) {
    const view = yield* c.views
      .slot("subagent", (ctx) => render({ controller: c, ctx }), { key, on: ["chat_changed", "agents_changed"] })
      .pipe(Effect.provideService(Scope.Scope, c.scope));
    entry = c.state.paneViews[key] = { chatId, view };
  }
  return entry.view;
});
const showInPane = Effect.fn("Chat.showInPane")(function* (
  c: Controller,
  chat: Chat,
  key: string,
  title: string,
  icon: string,
  pane: ViewHandle,
) {
  const client = c.tabs;
  // Without the tabs plugin there is no detail pane; the shared request the
  // baseline wrote would have had no reader either.
  if (!client) return;
  yield* client.open({ kind: "view", workspaceId: chat.workspaceId, path: key, title, icon, view: pane.id }).pipe(
    // A call that loses its provider on the way behaves like no reader.
    Effect.ignore,
  );
});
export const openWorkflow = Effect.fn("Chat.openWorkflow")(function* (c: Controller, chat: Chat, taskId: string) {
  const key = `workflow/${chat.id}/${taskId}`,
    pane = yield* paneView(c, key, chat.id, (v) => renderWorkflowPane(v, chat.id, taskId));
  const item = findTask(c.transcript(chat.id).items, taskId);
  yield* showInPane(
    c,
    chat,
    key,
    item ? item.task.name || item.task.title || "Workflow" : "Workflow",
    "workflow",
    pane,
  );
});
export const openSubagent = Effect.fn("Chat.openSubagent")(function* (
  c: Controller,
  chat: Chat,
  taskId: string,
): Effect.fn.Return<void, ChatError> {
  if (findTask(c.transcript(chat.id).items, taskId)?.task.workflow) return yield* openWorkflow(c, chat, taskId);
  const key = `subagent/${chat.id}/${taskId}`,
    pane = yield* paneView(c, key, chat.id, (v) => renderSubagentPane(v, chat.id, taskId));
  const item = findTask(c.transcript(chat.id).items, taskId);
  yield* showInPane(c, chat, key, item ? item.task.title || nameOf(item.task) : "Subagent", "bot", pane);
});
export const openAgents = Effect.fn("Chat.openAgents")(function* (c: Controller, chat: Chat) {
  const key = `agents/${chat.id}`;
  yield* showInPane(
    c,
    chat,
    key,
    "Agents",
    "network",
    yield* paneView(c, key, chat.id, (v) => renderAgentsPane(v, chat.id)),
  );
});

Versions

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <3105.1 KB5 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.