Official

tabs

The tabbed detail pane beside the chat: opened files and diffs (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/tabs@0.2.0

Permissions in 0.2.0

  • Read files fs.readMediumReads files in the listed places.Open workspace files in tabsPlaces: the open workspace
  • Read chats chats.readMediumReads your transcripts and chat lists.Reload the open file when an agent finishes a turn
  • Show panels ui.slotsLowShows views in the listed parts of the window.Draw the tabbed pane beside the chatSlots: detail

Files

main.ts2 KB
// The detail pane: tabs open through the convergence/tabs service, and, for
// third-party plugins, through the legacy shared openTab/closeTab requests.
// Views and subscriptions register synchronously at the start; the service
// is provided after them. The plugin scope owns request listeners and file
// reloads.
import * as Effect from "effect/Effect";
import * as Stream from "effect/Stream";
import { App, Events, Services, Views, runPlugin } from "convergence/effect";
import { Tabs } from "./controller.ts";
import { Tabs as TabsService } from "./contract.ts";
import { runFinished } from "./tabs.ts";
import { renderDetail } from "./views.ts";

const program = Effect.gen(function* () {
  const tabs = yield* Tabs;
  const views = yield* Views;
  tabs.views.push(yield* views.slot("detail", (ctx) => renderDetail(ctx, tabs), { on: ["app_state"] }));
  const app = yield* App;
  const events = yield* Events;
  yield* app.changes.pipe(
    Stream.runForEach((next) => Effect.forkScoped(tabs.followApp(next), { startImmediately: true })),
    Effect.forkScoped({ startImmediately: true }),
  );
  yield* events.stream("agent").pipe(
    Stream.runForEach((event) => {
      const tab = tabs.state.tabs.find((tab) => tab.id === tabs.state.active);
      return Effect.forkScoped(runFinished(event.kind) && tab?.kind === "file" ? tabs.open(tab) : Effect.void, {
        startImmediately: true,
      });
    }),
    Effect.forkScoped({ startImmediately: true }),
  );
  yield* tabs.followApp(yield* app.state);
  // The service goes last: everything above registers synchronously, and a
  // provider that arrives later only adds calls, never views.
  const services = yield* Services;
  const handle = yield* services.provide(TabsService, {
    open: (params, _ctx) => tabs.open(params).pipe(Effect.map((tab) => ({ id: tab.id }))),
    close: (params, _ctx) => tabs.closeByRef(params),
    active: (_params, _ctx) => tabs.activeRef(),
  });
  tabs.emitActive((tab) => handle.emit("activeChanged", tab));
});

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

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.