Files
AgentHub/client/src/components/lanes/git/BranchesButton.tsx
T
nntrivi2001 2cc492bb88 feat(git): Branches & tags panel — rename, branch from any ref, upstream, tags
GET /git/refs lists local branches (upstream, ahead/behind, last commit,
merged, held by a worktree, protected), remote branches grouped by remote
and tags (target, annotated message/tagger/date). New writes in
server/lib/git-refs.js, dispatched from git-ops.js: branch-rename,
branch-upstream (set/unset), tag-create (light/annotated), tag-delete
(confirm), tag-push (one tag to origin, no --tags, no force), and
branch-create with a looked-up from + switch on a clean tree. Every ref is
looked up, never passed through; repo-wide writes are repo-root only
(ESUBDIR) and refused during any operation (EMERGING).

Client: git/BranchManager + TagManager dialog opened from the git bar's
tags icon, en+vi (gitRefs namespace), keyboard navigable.

# Conflicts:
#	CLAUDE.md
#	client/src/components/lanes/LaneGitBar.tsx
#	docs/API.md
#	server/__tests__/lanes-api.test.js
#	server/routes/lanes.js
2026-11-02 12:00:55 +07:00

72 lines
2.0 KiB
TypeScript

/**
* @file The git bar's "Branches & tags" icon button, which owns the Branches
* panel (`BranchManager`) it opens — portalled to `document.body`, so no
* scrolling or transformed ancestor of the Changes panel can clip it. The
* panel closes when the bar starts showing another workspace.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useState } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { Tags } from "lucide-react";
import type { GitSource } from "../../../lib/api";
import { BranchManager } from "./BranchManager";
export function BranchesButton({
laneId,
session,
source,
dirty,
merging,
disabled,
onChanged,
}: {
laneId: number;
session?: string;
source?: GitSource;
dirty: number;
merging: boolean;
disabled?: boolean;
onChanged: () => void;
}) {
const { t } = useTranslation(["gitRefs"]);
const [open, setOpen] = useState(false);
const [shown, setShown] = useState(`${laneId}|${session ?? ""}|${source ?? ""}`);
const key = `${laneId}|${session ?? ""}|${source ?? ""}`;
if (shown !== key) {
setShown(key);
setOpen(false);
}
return (
<>
<button
type="button"
onClick={() => setOpen(true)}
disabled={disabled}
title={t("openPanelHint")}
aria-label={t("openPanel")}
aria-haspopup="dialog"
className="flex h-6 w-6 items-center justify-center rounded text-[11px] text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg-primary disabled:cursor-not-allowed disabled:opacity-40"
>
<Tags className="h-3.5 w-3.5" />
</button>
{open &&
createPortal(
<BranchManager
laneId={laneId}
session={session}
source={source}
dirty={dirty}
merging={merging}
onClose={() => setOpen(false)}
onChanged={onChanged}
/>,
document.body
)}
</>
);
}
export default BranchesButton;