Official

git

Built-in git 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/git@0.2.0

Permissions in 0.2.0

  • Run git commands git.writeHighRuns any git command.Stage, commit, switch branches and sync with the remote
  • Read chats chats.readMediumReads your transcripts and chat lists.Refresh when an agent finishes a turn
  • Read git status and diffs git.readLowReads git status and diffs.Show the repository's status and diffs, and follow its changes
  • Show panels ui.slotsLowShows views in the listed parts of the window.Show the source control button and panelSlots: railright

Files

controller.ts3.4 KB
// Own one repository status reader and one panel state in the plugin scope.
// Changes during a read coalesce into one follow-up read, just as before conversion.
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 { Host, Plugin, Services } from "convergence/effect";
import type { ViewHandle } from "convergence";
import type { Tabs } from "../tabs/contract.ts";
import { makeActions } from "./actions.ts";
import { initialState } from "./status.ts";

const makeGit = Effect.fn("Git.make")(function* () {
  const host = yield* Host;
  const plugin = yield* Plugin;
  const services = yield* Services;
  const scope = yield* Effect.scope;
  const state = initialState();
  const views: ViewHandle[] = [];
  const redraw = () => {
    for (const view of views) view.update();
  };
  let loading = false;
  let again = false;
  const refresh = Effect.fn("Git.refresh")(function* () {
    if (!state.workspaceId) return;
    if (loading) {
      again = true;
      return;
    }
    do {
      const workspaceId = state.workspaceId;
      if (!workspaceId) return;
      loading = true;
      yield* host.call("host/git.status", { workspaceId }).pipe(
        Effect.tap((status) =>
          Effect.sync(() => {
            if (workspaceId === state.workspaceId) Object.assign(state, { status, error: null });
          }),
        ),
        Effect.catchTag(["HostCallFailed", "NeedsReview", "PermissionNotGranted"], (error) =>
          Effect.sync(() => {
            if (workspaceId === state.workspaceId) state.error = error.message;
          }),
        ),
        Effect.ensuring(
          Effect.sync(() => {
            loading = false;
          }),
        ),
      );
      redraw();
      if (!again) return;
      again = false;
    } while (true);
  });
  const show = Effect.fn("Git.show")(function* (workspaceId: string | undefined) {
    if (!workspaceId || workspaceId === state.workspaceId) return;
    Object.assign(state, { workspaceId, status: null, error: null, confirm: null, menuOpen: false, branchOpen: false });
    redraw();
    // Older hosts lack watch; preserve their turn-finished/manual-refresh fallback.
    const watch: unknown = plugin.api.host.git.watch;
    if (typeof watch === "function")
      yield* Effect.forkIn(
        host
          .call("host/git.watch", { workspaceId })
          .pipe(
            Effect.catchTag(["HostCallFailed", "NeedsReview", "PermissionNotGranted"], (error) =>
              Effect.sync(() => console.warn(`git watch failed: ${error.message}`)),
            ),
          ),
        scope,
        { startImmediately: true },
      );
    yield* refresh();
  });
  const actions = yield* makeActions(state, refresh, redraw);
  // Diffs and files 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(() => {
        state.tabs = option._tag === "Some" ? option.value : undefined;
      }),
    ),
    Effect.forkIn(scope, { startImmediately: true }),
  );
  return { state, views, refresh, show, ...actions };
});

export class Git extends Context.Service<Git, Effect.Success<ReturnType<typeof makeGit>>>()("git/Git") {
  static readonly layer = Layer.effect(Git, makeGit());
}

Versions

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <313.4 KB4 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.