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
views/entries.ts4.7 KB
// Status rows, section folds and their explicit two-press discard affordance.
// HTM whitespace is significant; keep these templates out of Prettier's HTML formatter.
import * as Effect from "effect/Effect";
import { html as htm } from "convergence";
import type { Element } from "convergence";
import type { EffectViewContext } from "convergence/effect";
import type { Git } from "../controller.ts";
import { TONE } from "../status.ts";
import type { Entry, Section } from "../status.ts";
import { column, row, panelRow, hoverActions } from "./layout.ts";
// A status letter as the tree's status.
const STATUS: Readonly<Record<string, string>> = {
M: "modified",
T: "modified",
A: "added",
D: "deleted",
R: "renamed",
C: "renamed",
U: "untracked",
"!": "conflict",
};
export function entriesView(ctx: EffectViewContext<object>, actions: Git["Service"]) {
const { state } = actions;
function confirmed(key: string, effect: Effect.Effect<unknown, unknown>) {
if (state.confirm === key) {
state.confirm = null;
ctx.run(effect);
} else {
state.confirm = key;
ctx.update();
}
}
function action(id: string, icon: string, tooltip: string, onClick: () => void, danger = false) {
return htm`<button id=${id} icon=${icon} variant=${danger ? "danger" : "ghost"} small tooltip=${tooltip} disabled=${!!state.busy} onClick=${onClick} />`;
}
// A changed file's hover buttons: open, then stage or unstage, and a
// discard that asks twice.
function fileActions(e: Entry) {
const key = `${e.side}:${e.path}`;
const buttons: { icon: string; tooltip: string; onClick: () => void }[] = [];
if (e.letter !== "D")
buttons.push({ icon: "file", tooltip: "Open File", onClick: () => ctx.run(actions.openFile(e)) });
if (e.side === "staged") {
buttons.push({ icon: "minus", tooltip: "Unstage Changes", onClick: () => ctx.run(actions.unstage([e])) });
return buttons;
}
if (e.side === "changes") {
const armed = state.confirm === `discard:${key}`;
buttons.push({
icon: "undo-2",
tooltip: armed
? e.letter === "U"
? "Click again to delete the file"
: "Click again to discard"
: "Discard Changes",
onClick: () => confirmed(`discard:${key}`, actions.discard([e])),
});
}
buttons.push({
icon: "plus",
tooltip: e.side === "merge" ? "Mark Resolved (Stage)" : "Stage Changes",
onClick: () => ctx.run(actions.stage([e])),
});
return buttons;
}
// A section's files as the kernel's file tree: folders open, each file
// with its status (the name's colour and its letter) and buttons.
function fileTree(key: Section, entries: readonly Entry[]) {
const byPath = new Map(entries.map((e) => [e.path, e]));
const files = entries.map((e) => ({
path: e.path,
label: e.origPath ? `${e.origPath.split("/").pop()} → ${e.path.split("/").pop()}` : undefined,
status: STATUS[e.letter] ?? "modified",
badge: e.letter,
actions: fileActions(e).map((action) => ({ ...action, disabled: !!state.busy })),
}));
return htm`<tree id=${`git-tree-${key}`} fit openFolders files=${files} onSelect=${(ev: {
id: string;
folder: boolean;
}) => {
const e = byPath.get(ev.id);
if (!ev.folder && e) ctx.run(actions.openDiff(e));
}} />`;
}
function section(key: Section, title: string, entries: readonly Entry[], buttons: Element[]) {
if (!entries.length) return null;
const open = !state.collapsed[key];
const toggle = () => {
state.collapsed[key] = open;
ctx.update();
};
// The header sits on the tree's grid (its title where the folders'
// icons start) but reads as a heading: a taller row, small bold
// capitals in the muted tone, the count and the chevron at the end.
// While the pointer is on it the buttons take the count's place, ending
// where it ends, and the chevron stays in view.
return htm`
<div ...${column} gap=${0} padding="small">
<div ...${panelRow} id=${`git-section-${key}`} height=${30} gap=${6} onClick=${toggle}>
<text style="section" size=${11} weight=${700} tone="muted" truncate>${title}</text>
<spacer />
<div ...${row} position="relative" height="100%">
<div ...${hoverActions} right=${0} stopPropagation>${buttons}</div>
<div groupHover=${{ group: "hover", visibility: "hidden" }}><text style="body" size=${9.84} tone="muted">${entries.length}</text></div>
</div>
<icon name=${open ? "tree-chevron-down" : "tree-chevron-right"} tone="muted" />
</div>
${open ? fileTree(key, entries) : null}
</div>`;
}
return { action, section, confirmed };
}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.