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

views/controls.ts4.3 KB
// Primary action, branch picker and repository menu trees.
// HTM whitespace is significant; keep these templates out of Prettier's HTML formatter.
import { ui, html as htm } from "convergence";
import type { GitStatus } from "convergence";
import type { EffectViewContext } from "convergence/effect";
import type { Git } from "../controller.ts";
import { branchOptions, MENU, primaryAction } from "../status.ts";
import type { Groups } from "../status.ts";
import { column, panelRow, menuCard } from "./layout.ts";

export function controls(ctx: EffectViewContext<object>, actions: Git["Service"]) {
  const { state } = actions;
  function primaryButton(status: GitStatus, total: number) {
    const disabled = !!state.busy;
    if (primaryAction(status, total) === "commit") {
      return htm`<button id="git-commit" icon="check" label="Commit" variant="accent" disabled=${disabled || !state.message.trim()} onClick=${() => ctx.run(actions.commit())} />`;
    }
    if (primaryAction(status, total) === "publish") {
      return htm`<button id="git-publish" icon="cloud-upload" label="Publish Branch" variant="accent" disabled=${disabled} onClick=${() => ctx.run(actions.publish())} />`;
    }
    if (primaryAction(status, total) === "sync") {
      const counts = [status.behind ? `${status.behind}↓` : null, status.ahead ? `${status.ahead}↑` : null]
        .filter(Boolean)
        .join(" ");
      return htm`<button id="git-sync" icon="refresh-cw" label=${`Sync Changes ${counts}`} variant="accent" disabled=${disabled} onClick=${() => ctx.run(actions.sync())} />`;
    }
    return htm`<button id="git-commit" icon="check" label="Commit" variant="accent" disabled />`;
  }

  function branchPicker(status: GitStatus) {
    const { query: q, shown, create } = branchOptions(state.branches, state.branchQuery);
    const items = [
      ...(create
        ? [
            {
              id: "git-branch-create",
              label: `Create branch "${q}"`,
              icon: "plus",
              onClick: () => ctx.run(actions.checkout(q, true)),
            },
          ]
        : []),
      ...shown.map((b) => ({
        id: `git-branch-${b}`,
        label: b,
        icon: "git-branch",
        selected: b === status.branch,
        onClick: () => ctx.run(actions.checkout(b, false)),
      })),
    ];
    return htm`
      <popover id="git-branch-popover" open=${state.branchOpen} align="start" width=${280} onDismiss=${() => {
        state.branchOpen = false;
        ctx.update();
      }}>
        <div ...${panelRow} id="git-branch" onPress=${() => ctx.run(actions.openBranches())}>
          <icon name="git-branch" small tone="muted" />
          <text style="body" size="xs" tone="muted" truncate>${status.branch ?? "(no branch)"}</text>
        </div>
        <div ...${menuCard}>
          <input id="git-branch-query" value=${state.branchQuery} placeholder="Switch to or create a branch"
            onChange=${(e: { value: string }) => {
              state.branchQuery = e.value;
              ctx.update();
            }}
            onSubmit=${() => {
              if (create) ctx.run(actions.checkout(q, true));
              else if (shown[0]) ctx.run(actions.checkout(shown[0], false));
            }} />
          <div ...${column} gap="small" padding="small" maxHeight=${280} overflowY="scroll">
            ${items.length ? ui.list({ id: "git-branch-list", items }) : htm`<text style="muted">No branches.</text>`}
          </div>
        </div>
      </popover>`;
  }

  function menu(status: GitStatus, g: Groups) {
    const items = MENU.map((item) => ({
      id: `git-menu-${item.action}`,
      label: item.label,
      icon: item.icon,
      onClick: () => {
        state.menuOpen = false;
        ctx.update();
        ctx.run(actions.menuAction(item.action, status, g));
      },
    }));
    return htm`
      <popover id="git-menu" open=${state.menuOpen} align="end" width=${220} onDismiss=${() => {
        state.menuOpen = false;
        ctx.update();
      }}>
        <button id="git-more" icon="ellipsis" variant="ghost" small tooltip="More actions" disabled=${!!state.busy}
          onClick=${() => {
            state.menuOpen = !state.menuOpen;
            ctx.update();
          }} />
        <div ...${menuCard}>${ui.list({ id: "git-menu-list", items })}</div>
      </popover>`;
  }

  return { primaryButton, branchPicker, menu };
}

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.