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.ts4.1 KB
// Porcelain grouping and panel/menu choices, kept pure for regression tests.
// A path can appear on both sides of the index; conflicts belong only to merge.
import * as z from "zod";
import type { EffectClient } from "convergence/effect";
import type { AppState, GitEntry, GitStatus } from "convergence";
import type { Tabs } from "../tabs/contract.ts";

export const TONE: Readonly<Record<string, string>> = {
  M: "warning",
  T: "warning",
  A: "success",
  U: "success",
  D: "danger",
  R: "info",
  C: "info",
  "!": "danger",
};
const CONFLICT = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
export type Section = "merge" | "staged" | "changes";
export interface Entry extends GitEntry {
  letter: string;
  side: Section;
}
export type Groups = Record<Section, Entry[]>;
export interface GitState {
  workspaceId: string | null;
  status: GitStatus | null;
  error: string | null;
  message: string;
  busy: string | null;
  collapsed: Record<Section, boolean>;
  confirm: string | null;
  menuOpen: boolean;
  branchOpen: boolean;
  branches: string[];
  branchQuery: string;
  // The convergence/tabs client while its provider runs; diffs and files
  // open through it, and stay closed without it.
  tabs: EffectClient<typeof Tabs> | undefined;
}
export const initialState = (): GitState => ({
  workspaceId: null,
  status: null,
  error: null,
  message: "",
  busy: null,
  collapsed: { merge: false, staged: false, changes: false },
  confirm: null,
  menuOpen: false,
  branchOpen: false,
  branches: [],
  branchQuery: "",
  tabs: undefined,
});

export function groups(status: GitStatus): Groups {
  const result: Groups = { merge: [], staged: [], changes: [] };
  for (const entry of status.entries) {
    const x = entry.staged ?? " ",
      y = entry.unstaged ?? " ";
    if (CONFLICT.has(x + y)) result.merge.push({ ...entry, letter: "!", side: "merge" });
    else if (x === "?") result.changes.push({ ...entry, letter: "U", side: "changes" });
    else {
      if (x !== " ") result.staged.push({ ...entry, letter: x, side: "staged" });
      if (y !== " ") result.changes.push({ ...entry, letter: y, side: "changes" });
    }
  }
  return result;
}
export const pathsOf = (entries: readonly Entry[]): string[] =>
  entries.flatMap((entry) => (entry.origPath ? [entry.origPath, entry.path] : [entry.path]));
export const isOpen = (app: AppState): boolean =>
  app.shared?.rightPanel === "git" || app.shared?.rightPanelPreview === "git";

export function branchOptions(branches: readonly string[], query: string) {
  const q = query.trim();
  return {
    query: q,
    shown: branches.filter((branch) => branch.toLowerCase().includes(q.toLowerCase())),
    create: !!q && !branches.includes(q),
  };
}
export function primaryAction(status: GitStatus, total: number): "commit" | "publish" | "sync" | "idle" {
  if (total) return "commit";
  if (status.branch && !status.upstream && status.branch !== "HEAD (no branch)") return "publish";
  return status.ahead || status.behind ? "sync" : "idle";
}
export function discardPaths(entries: readonly Entry[]) {
  return {
    untracked: entries.filter((entry) => entry.letter === "U").map((entry) => entry.path),
    tracked: pathsOf(entries.filter((entry) => entry.letter !== "U")),
  };
}

export type MenuAction = "pull" | "push" | "fetch" | "amend" | "undo" | "stage-all" | "unstage-all" | "stash" | "pop";
export const MENU: readonly { action: MenuAction; label: string; icon: string }[] = [
  { action: "pull", label: "Pull", icon: "arrow-down" },
  { action: "push", label: "Push", icon: "arrow-up" },
  { action: "fetch", label: "Fetch", icon: "refresh-cw" },
  { action: "amend", label: "Commit (Amend)", icon: "git-commit-horizontal" },
  { action: "undo", label: "Undo Last Commit", icon: "undo-2" },
  { action: "stage-all", label: "Stage All Changes", icon: "plus" },
  { action: "unstage-all", label: "Unstage All Changes", icon: "minus" },
  { action: "stash", label: "Stash", icon: "archive" },
  { action: "pop", label: "Pop Latest Stash", icon: "archive-restore" },
];

const finished = z.object({ event: z.literal("run_finished") });
export const runFinished = (kind: unknown): boolean => finished.safeParse(kind).success;

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.