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