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
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
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 13.4 KB | 4 permissions | Listed |
No comments yet.