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

main.ts5.2 KB
// Register title then center synchronously, preserving retained view identity across reloads.
// The convergence/chat service is provided last; the tabs client is
// followed while its provider runs. The plugin scope owns every listener,
// background load and elapsed-time loop.
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Stream from "effect/Stream";
import { App, Host, Kernel, Notify, Plugin, Services, Settings, Views, runPlugin } from "convergence/effect";
import { Controller } from "./controller.ts";
import { Chat as ChatService } from "./contract.ts";
import type { Tabs } from "../tabs/contract.ts";
import { createModelPicker } from "./models.ts";
import { renderCenter } from "./views/panes.ts";
import { renderTitle } from "./views/title.ts";
import { registerEarlyCommands } from "./commands.ts";
import { listen } from "./events.ts";
import { InputScheduler } from "./scheduler-contract.ts";
import { bindSettings, renderSettings } from "./settings.ts";
import { policyOf } from "./scheduler-policy.ts";
const make = Effect.fn("Chat.controller")(function* () {
  return new Controller({
    api: (yield* Plugin).api,
    host: yield* Host,
    kernel: yield* Kernel,
    app: yield* App,
    notify: yield* Notify,
    views: yield* Views,
    scope: yield* Effect.scope,
  });
});
class ChatController extends Context.Service<ChatController, Controller>()("chat/Controller") {
  static readonly layer = Layer.effect(ChatController, make());
}
const program = Effect.gen(function* () {
  const c = yield* ChatController;
  yield* registerEarlyCommands(c);
  c.titleView = yield* c.views.slot("title", (ctx) => renderTitle({ controller: c, ctx }), {
    on: ["chat_changed", "agents_changed", "app_state", "workspaces_changed"],
  });
  c.center = yield* c.views.slot("center", (ctx) => renderCenter({ controller: c, ctx }), {
    on: ["chat_changed", "agents_changed", "app_state"],
  });
  yield* listen(c);
  const services = yield* Services;
  // Detail views need the tabs plugin; without it they stay closed, as the
  // shared request they replaced would have had no reader.
  yield* services.when<typeof Tabs>("convergence/tabs").pipe(
    Stream.runForEach((option) =>
      Effect.sync(() => {
        c.tabs = option._tag === "Some" ? option.value : undefined;
      }),
    ),
    Effect.forkScoped({ startImmediately: true }),
  );
  const fork = <A, E>(effect: Effect.Effect<A, E>) => Effect.forkIn(effect, c.scope, { startImmediately: true });
  c.modelPicker = createModelPicker(c);
  yield* fork(c.modelPicker.load);
  yield* fork(c.loadRecent());
  yield* fork(c.restoreQueues());
  yield* fork(c.restoreUnsent());
  // The service goes last: providing waits for the host, and the loads above
  // must not wait for it. A consumer reads the state with queued() and
  // draftTitles() once the service is there; events count from then on.
  const handle = yield* services.provide(ChatService, {
    queued: () => Effect.succeed(c.state.chatQueue.map((queued) => queued.chatId)),
    draftTitles: () => Effect.succeed({ ...c.state.draftTitles }),
  });
  c.emitQueued = (queued) => handle.emit("queuedChanged", queued);
  c.emitDrafts = (drafts) => handle.emit("draftTitlesChanged", drafts);
  // Restores forked above may have set queue/draft state before the service
  // existed, when their announces were no-ops. Emit what they found, like
  // the baseline's shares did, so consumers see the restored state.
  // Restores still running will emit themselves, since the service is up;
  // this only covers what is already there, so there is no duplicate.
  if (c.state.chatQueue.length) c.emitQueued(c.state.chatQueue.map((queued) => queued.chatId));
  if (Object.keys(c.state.draftTitles).length) c.emitDrafts({ ...c.state.draftTitles });
  yield* services.provide(InputScheduler, {
    policy: () => Effect.succeed({ version: 1 as const, release: "settled-success-fifo" as const }),
  });
  c.settingsView = yield* c.views.slot("chat-settings", (ctx) => renderSettings({ controller: c, ctx }), {
    on: ["agents_changed", "plugin_settings_changed"],
  });
  yield* Effect.acquireRelease(
    Effect.sync(() => bindSettings(c.settingsView?.id)),
    () => Effect.sync(() => bindSettings(undefined)),
  );
  const settings = yield* Settings;
  yield* settings.changes.pipe(
    Stream.runForEach(() => fork(c.scheduler.updatePreferences())),
    Effect.forkScoped({ startImmediately: true }),
  );
  yield* fork(c.scheduler.restore());
  // A bounded scoped clock reconstructs deadlines after reload and catches
  // sleep/wake. It never polls with prompts and does no work without a wait.
  yield* fork(
    Effect.gen(function* () {
      while (true) {
        yield* Effect.sleep("1 second");
        yield* c.scheduler.evaluate();
        for (const snapshot of Object.values(c.scheduler.snapshots))
          if (c.scheduler.activeLimits[snapshot.chatId] || policyOf(snapshot).wait) {
            // Only visible wait notices need countdown repainting.
            if (c.api.app.state().chatId === snapshot.chatId) c.refresh(snapshot.chatId);
          }
      }
    }),
  );
});
export const activate = runPlugin(ChatController.layer, program);

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.