Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 175 additions & 106 deletions apps/web/src/components/GitActionsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,16 @@ import {
} from "~/components/ui/dialog";
import { Group, GroupSeparator } from "~/components/ui/group";
import { Input } from "~/components/ui/input";
import { Menu, MenuItem, MenuPopup, MenuTrigger } from "~/components/ui/menu";
import {
Menu,
MenuItem,
MenuItemLabel,
MenuPopup,
MenuSub,
MenuSubTrigger,
MenuSubPopup,
MenuTrigger,
} from "~/components/ui/menu";
import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";
import { ScrollArea } from "~/components/ui/scroll-area";
import { Textarea } from "~/components/ui/textarea";
Expand Down Expand Up @@ -108,6 +117,7 @@ import { useOpenLink } from "~/browser/useOpenLink";
import { useOpenPrLink } from "~/lib/openPullRequestLink";

interface GitActionsControlProps {
presentation?: "toolbar" | "menu";
gitCwd: string | null;
activeThreadRef: ScopedThreadRef | null;
draftId?: DraftId;
Expand Down Expand Up @@ -374,24 +384,25 @@ function GitActionItemIcon({
function GitQuickActionIcon({
quickAction,
SourceControlIcon,
className = "size-3.5",
}: {
quickAction: GitQuickAction;
className?: string;
SourceControlIcon: ReturnType<typeof getSourceControlPresentation>["Icon"];
}) {
const iconClassName = "size-3.5";
if (quickAction.kind === "open_pr") return <SourceControlIcon className={iconClassName} />;
if (quickAction.kind === "open_publish") return <CloudUploadIcon className={iconClassName} />;
if (quickAction.kind === "run_pull") return <CloudDownloadIcon className={iconClassName} />;
if (quickAction.kind === "open_pr") return <SourceControlIcon className={className} />;
if (quickAction.kind === "open_publish") return <CloudUploadIcon className={className} />;
if (quickAction.kind === "run_pull") return <CloudDownloadIcon className={className} />;
if (quickAction.kind === "run_action") {
if (quickAction.action === "commit") return <GitCommitIcon className={iconClassName} />;
if (quickAction.action === "commit") return <GitCommitIcon className={className} />;
if (quickAction.action === "push" || quickAction.action === "commit_push") {
return <CloudUploadIcon className={iconClassName} />;
return <CloudUploadIcon className={className} />;
}
return <SourceControlIcon className={iconClassName} />;
return <SourceControlIcon className={className} />;
}
if (quickAction.label === "Commit") return <GitCommitIcon className={iconClassName} />;
if (quickAction.label === "Push") return <CloudUploadIcon className={iconClassName} />;
return <InfoIcon className={iconClassName} />;
if (quickAction.label === "Commit") return <GitCommitIcon className={className} />;
if (quickAction.label === "Push") return <CloudUploadIcon className={className} />;
return <InfoIcon className={className} />;
}

interface PublishRepositoryDialogProps {
Expand Down Expand Up @@ -944,6 +955,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
}

export default function GitActionsControl({
presentation = "toolbar",
gitCwd,
activeThreadRef,
draftId,
Expand Down Expand Up @@ -1632,33 +1644,161 @@ export default function GitActionsControl({

const canPublishRepository = isRepo && gitStatusForActions !== null && !hasPrimaryRemote;

if (!gitCwd) return null;

return (
const initializeGit = () => {
void (async () => {
const result = await initAction.run();
if (result._tag === "Success" || isAtomCommandInterrupted(result)) {
return;
}
const error = squashAtomCommandFailure(result);
toastManager.add(
stackedThreadToast({
type: "error",
title: "Git initialization failed",
description: error instanceof Error ? error.message : "An error occurred.",
...(threadToastData !== undefined ? { data: threadToastData } : {}),
}),
);
})();
};
const gitItems = (
<>
{!isRepo ? (
<Button
variant="outline"
size="xs"
disabled={initAction.isPending}
{gitActionMenuItems.map((item) => {
const disabledReason = getMenuActionDisabledReason({
item,
gitStatus: gitStatusForActions,
isBusy: isGitActionRunning,
hasPrimaryRemote,
});
if (item.disabled && disabledReason && presentation === "menu") {
return (
<div key={`${item.id}-${item.label}`}>
<MenuItem density={presentation === "menu" ? "touch" : "default"} disabled>
<GitActionItemIcon icon={item.icon} SourceControlIcon={SourceControlIcon} />
<MenuItemLabel>{item.label}</MenuItemLabel>
</MenuItem>
<p className="max-w-64 px-2 pb-2 text-xs text-muted-foreground">{disabledReason}</p>
</div>
);
}
if (item.disabled && disabledReason) {
return (
<Popover key={`${item.id}-${item.label}`}>
<PopoverTrigger
openOnHover
nativeButton={false}
render={<span className="block w-max cursor-not-allowed" />}
>
<MenuItem
density={presentation === "menu" ? "touch" : "default"}
className="w-full"
disabled
>
<GitActionItemIcon icon={item.icon} SourceControlIcon={SourceControlIcon} />
<MenuItemLabel>{item.label}</MenuItemLabel>
</MenuItem>
</PopoverTrigger>
<PopoverPopup tooltipStyle side="left" align="center">
{disabledReason}
</PopoverPopup>
</Popover>
);
}

return (
<MenuItem
density={presentation === "menu" ? "touch" : "default"}
key={`${item.id}-${item.label}`}
disabled={item.disabled}
onClick={() => {
openDialogForMenuItem(item);
}}
>
<GitActionItemIcon icon={item.icon} SourceControlIcon={SourceControlIcon} />
<MenuItemLabel>{item.label}</MenuItemLabel>
</MenuItem>
);
})}
{canPublishRepository ? (
<MenuItem
density={presentation === "menu" ? "touch" : "default"}
disabled={isGitActionRunning}
onClick={() => {
void (async () => {
const result = await initAction.run();
if (result._tag === "Success" || isAtomCommandInterrupted(result)) {
return;
}
const error = squashAtomCommandFailure(result);
toastManager.add(
stackedThreadToast({
type: "error",
title: "Git initialization failed",
description: error instanceof Error ? error.message : "An error occurred.",
...(threadToastData !== undefined ? { data: threadToastData } : {}),
}),
);
})();
setIsPublishDialogOpen(true);
}}
>
<CloudUploadIcon />
<MenuItemLabel>Publish repository...</MenuItemLabel>
</MenuItem>
) : null}
{gitStatusForActions?.refName === null && (
<p className="px-2 py-1.5 text-xs text-warning">
Detached HEAD: create and check out a branch to enable push and pull request actions.
</p>
)}
{gitStatusForActions &&
gitStatusForActions.refName !== null &&
!gitStatusForActions.hasWorkingTreeChanges &&
gitStatusForActions.behindCount > 0 &&
gitStatusForActions.aheadCount === 0 && (
<p className="px-2 py-1.5 text-xs text-warning">Behind upstream. Pull/rebase first.</p>
)}
{gitStatusError && <p className="px-2 py-1.5 text-xs text-destructive">{gitStatusError}</p>}
</>
);

if (!gitCwd) return null;

return (
<>
{presentation === "menu" ? (
!isRepo ? (
<MenuItem
density={presentation === "menu" ? "touch" : "default"}

disabled={initAction.isPending}
onClick={initializeGit}
>
<GitBranchPlusIcon className="size-4" />
<MenuItemLabel>
{initAction.isPending ? "Initializing..." : "Initialize Git"}
</MenuItemLabel>
</MenuItem>
) : (
<>
<MenuItem
density={presentation === "menu" ? "touch" : "default"}

disabled={isGitActionRunning || quickAction.disabled || !!quickActionDisabledReason}
onClick={runQuickAction}
>
<GitQuickActionIcon
className="size-4"
quickAction={quickAction}
SourceControlIcon={SourceControlIcon}
/>
<MenuItemLabel>{quickAction.label}</MenuItemLabel>
</MenuItem>
{quickActionDisabledReason && (
<p className="max-w-64 px-2 py-1.5 text-xs text-warning">
{quickActionDisabledReason}
</p>
)}
<MenuSub
onOpenChange={(open) => {
if (open) requestVcsStatusRefresh(refreshVcsStatus, activeEnvironmentId, gitCwd);
}}
>
<MenuSubTrigger density="touch" disabled={isGitActionRunning}>
<SourceControlIcon className="size-4" />
<MenuItemLabel>Git actions</MenuItemLabel>
</MenuSubTrigger>
<MenuSubPopup className="min-w-32 max-w-[calc(100vw-2rem)]">{gitItems}</MenuSubPopup>
</MenuSub>
</>
)
) : !isRepo ? (
<Button variant="outline" size="xs" disabled={initAction.isPending} onClick={initializeGit}>
<GitBranchPlusIcon className="size-3.5" aria-hidden />
<span className="ml-0.5">
{initAction.isPending ? "Initializing..." : "Initialize Git"}
Expand Down Expand Up @@ -1720,78 +1860,7 @@ export default function GitActionsControl({
<ChevronDownIcon aria-hidden="true" className="size-4" />
</MenuTrigger>
<MenuPopup align="end" className="w-full">
{gitActionMenuItems.map((item) => {
const disabledReason = getMenuActionDisabledReason({
item,
gitStatus: gitStatusForActions,
isBusy: isGitActionRunning,
hasPrimaryRemote,
});
if (item.disabled && disabledReason) {
return (
<Popover key={`${item.id}-${item.label}`}>
<PopoverTrigger
openOnHover
nativeButton={false}
render={<span className="block w-max cursor-not-allowed" />}
>
<MenuItem className="w-full" disabled>
<GitActionItemIcon
icon={item.icon}
SourceControlIcon={SourceControlIcon}
/>
{item.label}
</MenuItem>
</PopoverTrigger>
<PopoverPopup tooltipStyle side="left" align="center">
{disabledReason}
</PopoverPopup>
</Popover>
);
}

return (
<MenuItem
key={`${item.id}-${item.label}`}
disabled={item.disabled}
onClick={() => {
openDialogForMenuItem(item);
}}
>
<GitActionItemIcon icon={item.icon} SourceControlIcon={SourceControlIcon} />
{item.label}
</MenuItem>
);
})}
{canPublishRepository ? (
<MenuItem
disabled={isGitActionRunning}
onClick={() => {
setIsPublishDialogOpen(true);
}}
>
<CloudUploadIcon />
Publish repository...
</MenuItem>
) : null}
{gitStatusForActions?.refName === null && (
<p className="px-2 py-1.5 text-xs text-warning">
Detached HEAD: create and check out a branch to enable push and pull request
actions.
</p>
)}
{gitStatusForActions &&
gitStatusForActions.refName !== null &&
!gitStatusForActions.hasWorkingTreeChanges &&
gitStatusForActions.behindCount > 0 &&
gitStatusForActions.aheadCount === 0 && (
<p className="px-2 py-1.5 text-xs text-warning">
Behind upstream. Pull/rebase first.
</p>
)}
{gitStatusError && (
<p className="px-2 py-1.5 text-xs text-destructive">{gitStatusError}</p>
)}
{gitItems}
</MenuPopup>
</Menu>
</Group>
Expand Down
Loading
Loading