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

status.test.ts4.1 KB
// Git status sections and menu/picker decisions, independent of the host.
import assert from "node:assert/strict";
import { test } from "node:test";
import type { GitEntry, GitStatus } from "convergence";
import {
  branchOptions,
  discardPaths,
  groups,
  initialState,
  isOpen,
  MENU,
  pathsOf,
  primaryAction,
  runFinished,
  TONE,
} from "./status.ts";
const status = (entries: GitEntry[] = []): GitStatus => ({
  entries,
  branch: "main",
  upstream: "origin/main",
  ahead: 0,
  behind: 0,
  isRepo: true,
});

test("all seven conflict codes occupy only the merge section", () => {
  const entries = ["DD", "AU", "UD", "UA", "DU", "AA", "UU"].map((code) => ({
    path: code,
    staged: code[0],
    unstaged: code[1],
  }));
  const grouped = groups(status(entries));
  assert.equal(grouped.merge.length, 7);
  assert.deepEqual(grouped.staged, []);
  assert.deepEqual(grouped.changes, []);
  assert.ok(grouped.merge.every((entry) => entry.side === "merge" && entry.letter === "!"));
});
test("staged and unstaged edits both appear, untracked is U, and null means unchanged", () => {
  const entries = [
    { path: "both", staged: "M", unstaged: "D" },
    { path: "new", staged: "?", unstaged: "?" },
    { path: "clean", staged: null, unstaged: null },
  ];
  const grouped = groups(status(entries));
  assert.deepEqual(
    grouped.staged.map((entry) => [entry.path, entry.letter]),
    [["both", "M"]],
  );
  assert.deepEqual(
    grouped.changes.map((entry) => [entry.path, entry.letter]),
    [
      ["both", "D"],
      ["new", "U"],
    ],
  );
  assert.deepEqual(entries[0], { path: "both", staged: "M", unstaged: "D" });
});
test("rename commands include old and new paths; discard separates tracked and untracked", () => {
  const grouped = groups(
    status([
      { path: "new", origPath: "old", staged: "R", unstaged: "M" },
      { path: "fresh", staged: "?", unstaged: "?" },
    ]),
  );
  assert.deepEqual(pathsOf(grouped.staged), ["old", "new"]);
  assert.deepEqual(discardPaths(grouped.changes), { tracked: ["old", "new"], untracked: ["fresh"] });
});
test("primary action prioritizes commit, then publish, then sync, then idle", () => {
  const s = status();
  assert.equal(primaryAction(s, 1), "commit");
  assert.equal(primaryAction(s, 0), "idle");
  assert.equal(primaryAction({ ...s, upstream: null, ahead: 1 }, 0), "publish");
  assert.equal(primaryAction({ ...s, ahead: 1 }, 0), "sync");
  assert.equal(primaryAction({ ...s, behind: 1 }, 0), "sync");
  assert.equal(primaryAction({ ...s, branch: "HEAD (no branch)", upstream: null }, 0), "idle");
  assert.equal(primaryAction({ ...s, branch: null, upstream: null }, 0), "idle");
});
test("branch filtering is case-insensitive but creation retains exact-name semantics", () => {
  assert.deepEqual(branchOptions(["main", "Feature"], "  fea  "), { query: "fea", shown: ["Feature"], create: true });
  assert.equal(branchOptions(["main"], "main").create, false);
  assert.equal(branchOptions(["main"], "MAIN").create, true);
  assert.equal(branchOptions([], " ").create, false);
});
test("menu ids, labels and order remain the original source-control menu", () => {
  assert.deepEqual(
    MENU.map((item) => item.action),
    ["pull", "push", "fetch", "amend", "undo", "stage-all", "unstage-all", "stash", "pop"],
  );
  assert.deepEqual(
    MENU.map((item) => item.label),
    [
      "Pull",
      "Push",
      "Fetch",
      "Commit (Amend)",
      "Undo Last Commit",
      "Stage All Changes",
      "Unstage All Changes",
      "Stash",
      "Pop Latest Stash",
    ],
  );
  assert.equal(TONE.D, "danger");
  assert.equal(TONE.R, "info");
  assert.equal(TONE.U, "success");
});
test("panel visibility, fresh fold state and turn-finished selection", () => {
  assert.equal(isOpen({ shared: { rightPanelPreview: "git" } }), true);
  assert.equal(isOpen({ shared: { rightPanel: "git" } }), true);
  assert.equal(isOpen({}), false);
  const a = initialState(),
    b = initialState();
  a.collapsed.merge = true;
  assert.equal(b.collapsed.merge, false);
  assert.equal(runFinished({ event: "run_finished" }), true);
  assert.equal(runFinished(null), false);
});

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.