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

main.ts2.6 KB
// Register the sidebar synchronously, its commands, and the event
// subscriptions that keep its state current; the initial refresh runs in
// the plugin scope.
import * as Effect from "effect/Effect";
import * as Scope from "effect/Scope";
import * as Stream from "effect/Stream";
import { Commands, Events, Views, runPlugin } from "convergence/effect";
import { Sidebar } from "./controller.ts";
import { renderSidebar } from "./views.ts";

const program = Effect.gen(function* () {
  const sidebar = yield* Sidebar;
  const views = yield* Views;
  // Views register synchronously at the start of the program so reload
  // keeps the view's identity (same slot, same order).
  sidebar.views.push(
    yield* views.slot("left", (ctx) => renderSidebar(ctx, sidebar), {
      on: ["workspaces_changed", "chat_changed", "chat_removed", "agents_changed", "app_state"],
    }),
  );
  const commands = yield* Commands;
  yield* commands.register("sidebar.newChat", sidebar.newChatCommand());
  yield* commands.register("sidebar.toggleChatCompleted", sidebar.toggleCompletedCommand());
  yield* commands.register("sidebar.addWorkspace", sidebar.addWorkspace());
  for (let n = 1; n <= 9; n++) yield* commands.register(`sidebar.openChat${n}`, sidebar.selectListedCommand(n - 1));
  const events = yield* Events;
  // Event followers are fire-and-forget like the original's async handlers:
  // a failed refresh leaves the state for the next event, and the rest of
  // the plugin keeps working.
  const follow = <A, E>(
    stream: Stream.Stream<A, E>,
    handle: (event: A) => Effect.Effect<void, unknown>,
  ): Effect.Effect<void, never, Scope.Scope> =>
    stream.pipe(
      Stream.mapEffect((event) => handle(event).pipe(Effect.ignore), { concurrency: "unbounded" }),
      Stream.runDrain,
      Effect.forkScoped,
    );
  yield* follow(events.stream("workspaces_changed"), () => sidebar.refresh());
  yield* follow(events.stream("agents_changed"), () => sidebar.refresh());
  yield* follow(events.stream("chat_changed"), (event) => sidebar.chatChanged(event));
  yield* follow(events.stream("chat_removed"), (event) => sidebar.chatRemoved(event));
  yield* follow(events.stream("transcript_reset"), (event) => sidebar.transcriptReset(event));
  yield* follow(events.stream("agent"), (event) => sidebar.agentEvent(event));
  // The first load runs in the program fiber, like the original's refresh()
  // at the end of activate: its list calls leave before activate returns.
  // A failed load keeps the previous state, and the next event retries.
  yield* sidebar.refresh().pipe(Effect.ignore);
});

export const activate = runPlugin(Sidebar.layer, program);

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.