refactor(web): consolidate commit and PR dialogs to use vcs-dialogs (#426)
This commit is contained in:
@@ -1,16 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
IconGitCommit,
|
||||
IconGitPullRequest,
|
||||
IconLoader2,
|
||||
IconCheck,
|
||||
IconSparkles,
|
||||
} from "@tabler/icons-react";
|
||||
import { IconGitCommit, IconLoader2, IconCheck } from "@tabler/icons-react";
|
||||
|
||||
import { Button } from "@kandev/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -29,7 +22,6 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@kandev/ui/alert-dialog";
|
||||
import { Checkbox } from "@kandev/ui/checkbox";
|
||||
import { Label } from "@kandev/ui/label";
|
||||
import { Input } from "@kandev/ui/input";
|
||||
import { Textarea } from "@kandev/ui/textarea";
|
||||
@@ -74,325 +66,6 @@ export function DiscardDialog({
|
||||
);
|
||||
}
|
||||
|
||||
// --- Generate Button ---
|
||||
|
||||
type GenerateButtonProps = {
|
||||
onClick: () => void;
|
||||
isGenerating: boolean;
|
||||
disabled?: boolean;
|
||||
tooltip: string;
|
||||
};
|
||||
|
||||
function GenerateButton({ onClick, isGenerating, disabled, tooltip }: GenerateButtonProps) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 cursor-pointer"
|
||||
onClick={onClick}
|
||||
disabled={disabled || isGenerating}
|
||||
>
|
||||
{isGenerating ? (
|
||||
<IconLoader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<IconSparkles className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Commit Dialog ---
|
||||
|
||||
type CommitDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
commitMessage: string;
|
||||
onCommitMessageChange: (value: string) => void;
|
||||
onCommit: () => void;
|
||||
isLoading: boolean;
|
||||
stagedFileCount: number;
|
||||
stagedAdditions: number;
|
||||
stagedDeletions: number;
|
||||
isAmend?: boolean;
|
||||
onAmendChange?: (amend: boolean) => void;
|
||||
lastCommitMessage?: string | null;
|
||||
onGenerateMessage?: () => void;
|
||||
isGenerating?: boolean;
|
||||
};
|
||||
|
||||
export function CommitDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
commitMessage,
|
||||
onCommitMessageChange,
|
||||
onCommit,
|
||||
isLoading,
|
||||
stagedFileCount,
|
||||
stagedAdditions,
|
||||
stagedDeletions,
|
||||
isAmend = false,
|
||||
onAmendChange,
|
||||
lastCommitMessage,
|
||||
onGenerateMessage,
|
||||
isGenerating,
|
||||
}: CommitDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<IconGitCommit className="h-5 w-5" />
|
||||
{isAmend ? "Amend Commit" : "Commit Changes"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<CommitDialogStats
|
||||
stagedFileCount={stagedFileCount}
|
||||
stagedAdditions={stagedAdditions}
|
||||
stagedDeletions={stagedDeletions}
|
||||
/>
|
||||
{onAmendChange && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="amend-checkbox"
|
||||
checked={isAmend}
|
||||
onCheckedChange={(checked) => {
|
||||
onAmendChange(checked === true);
|
||||
// Pre-fill with last commit message when enabling amend
|
||||
if (checked === true && lastCommitMessage && !commitMessage.trim()) {
|
||||
onCommitMessageChange(lastCommitMessage);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="amend-checkbox" className="text-sm cursor-pointer">
|
||||
Amend previous commit
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
placeholder={isAmend ? "Enter new commit message..." : "Enter commit message..."}
|
||||
value={commitMessage}
|
||||
onChange={(e) => onCommitMessageChange(e.target.value)}
|
||||
autoFocus
|
||||
className="pr-10"
|
||||
/>
|
||||
{onGenerateMessage && (
|
||||
<div className="absolute right-1.5 top-1.5">
|
||||
<GenerateButton
|
||||
onClick={onGenerateMessage}
|
||||
isGenerating={isGenerating ?? false}
|
||||
disabled={stagedFileCount === 0}
|
||||
tooltip="Generate commit message with AI"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button onClick={onCommit} disabled={!commitMessage.trim() || isLoading}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<IconLoader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
{isAmend ? "Amending..." : "Committing..."}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconCheck className="h-4 w-4 mr-2" />
|
||||
{isAmend ? "Amend" : "Commit"}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function CommitDialogStats({
|
||||
stagedFileCount,
|
||||
stagedAdditions,
|
||||
stagedDeletions,
|
||||
}: {
|
||||
stagedFileCount: number;
|
||||
stagedAdditions: number;
|
||||
stagedDeletions: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{stagedFileCount > 0 ? (
|
||||
<span>
|
||||
<span className="font-medium text-foreground">{stagedFileCount}</span> staged file
|
||||
{stagedFileCount !== 1 ? "s" : ""}
|
||||
{(stagedAdditions > 0 || stagedDeletions > 0) && (
|
||||
<span className="ml-2">
|
||||
(<span className="text-green-600">+{stagedAdditions}</span>
|
||||
{" / "}
|
||||
<span className="text-red-600">-{stagedDeletions}</span>)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span>No staged files to commit</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Create PR Dialog ---
|
||||
|
||||
type PRDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
prTitle: string;
|
||||
onPrTitleChange: (value: string) => void;
|
||||
prBody: string;
|
||||
onPrBodyChange: (value: string) => void;
|
||||
prDraft: boolean;
|
||||
onPrDraftChange: (value: boolean) => void;
|
||||
onCreatePR: () => void;
|
||||
isLoading: boolean;
|
||||
displayBranch: string | null;
|
||||
baseBranch: string | undefined;
|
||||
onGenerateDescription?: () => void;
|
||||
isGenerating?: boolean;
|
||||
};
|
||||
|
||||
export function PRDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
prTitle,
|
||||
onPrTitleChange,
|
||||
prBody,
|
||||
onPrBodyChange,
|
||||
prDraft,
|
||||
onPrDraftChange,
|
||||
onCreatePR,
|
||||
isLoading,
|
||||
displayBranch,
|
||||
baseBranch,
|
||||
onGenerateDescription,
|
||||
isGenerating,
|
||||
}: PRDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<IconGitPullRequest className="h-5 w-5" />
|
||||
Create Pull Request
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
{displayBranch && <PRBranchInfo displayBranch={displayBranch} baseBranch={baseBranch} />}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="changes-pr-title" className="text-sm">
|
||||
Title
|
||||
</Label>
|
||||
<input
|
||||
id="changes-pr-title"
|
||||
type="text"
|
||||
placeholder="Pull request title..."
|
||||
value={prTitle}
|
||||
onChange={(e) => onPrTitleChange(e.target.value)}
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="changes-pr-body" className="text-sm">
|
||||
Description
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
id="changes-pr-body"
|
||||
placeholder="Describe your changes..."
|
||||
value={prBody}
|
||||
onChange={(e) => onPrBodyChange(e.target.value)}
|
||||
rows={6}
|
||||
className="resize-none max-h-[200px] overflow-y-auto pr-10"
|
||||
/>
|
||||
{onGenerateDescription && (
|
||||
<div className="absolute right-1.5 top-1.5">
|
||||
<GenerateButton
|
||||
onClick={onGenerateDescription}
|
||||
isGenerating={isGenerating ?? false}
|
||||
tooltip="Generate PR description with AI"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="changes-pr-draft"
|
||||
checked={prDraft}
|
||||
onCheckedChange={(checked) => onPrDraftChange(checked === true)}
|
||||
/>
|
||||
<Label htmlFor="changes-pr-draft" className="text-sm cursor-pointer">
|
||||
Create as draft
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button onClick={onCreatePR} disabled={!prTitle.trim() || isLoading}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<IconLoader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconGitPullRequest className="h-4 w-4 mr-2" />
|
||||
Create PR
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function PRBranchInfo({
|
||||
displayBranch,
|
||||
baseBranch,
|
||||
}: {
|
||||
displayBranch: string;
|
||||
baseBranch: string | undefined;
|
||||
}) {
|
||||
return (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{baseBranch ? (
|
||||
<span>
|
||||
Creating PR from <span className="font-medium text-foreground">{displayBranch}</span>{" "}
|
||||
→ <span className="font-medium text-foreground">{baseBranch}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
Creating PR from <span className="font-medium text-foreground">{displayBranch}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Amend Commit Message Dialog ---
|
||||
|
||||
type AmendDialogProps = {
|
||||
@@ -555,7 +228,7 @@ export function ResetDialog({
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline" disabled={isLoading}>
|
||||
<Button type="button" variant="outline" disabled={isLoading} className="cursor-pointer">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
@@ -88,16 +88,13 @@ export function useChangesGitHandlers(
|
||||
};
|
||||
}
|
||||
|
||||
function useChangesDiscardCommitHandlers(
|
||||
function useChangesDiscardAmendHandlers(
|
||||
gitOps: GitOps,
|
||||
toast: Toast,
|
||||
handleGitOperation: GitOperationFn,
|
||||
) {
|
||||
const [showDiscardDialog, setShowDiscardDialog] = useState(false);
|
||||
const [fileToDiscard, setFileToDiscard] = useState<string | null>(null);
|
||||
const [commitDialogOpen, setCommitDialogOpen] = useState(false);
|
||||
const [commitMessage, setCommitMessage] = useState("");
|
||||
const [isAmendCommit, setIsAmendCommit] = useState(false);
|
||||
|
||||
const handleDiscardClick = useCallback((filePath: string) => {
|
||||
setFileToDiscard(filePath);
|
||||
@@ -125,23 +122,6 @@ function useChangesDiscardCommitHandlers(
|
||||
}
|
||||
}, [fileToDiscard, gitOps, toast]);
|
||||
|
||||
const handleOpenCommitDialog = useCallback(() => {
|
||||
setCommitMessage("");
|
||||
setIsAmendCommit(false);
|
||||
setCommitDialogOpen(true);
|
||||
}, []);
|
||||
const handleCommit = useCallback(async () => {
|
||||
if (!commitMessage.trim()) return;
|
||||
setCommitDialogOpen(false);
|
||||
const operationName = isAmendCommit ? "Amend commit" : "Commit";
|
||||
await handleGitOperation(
|
||||
() => gitOps.commit(commitMessage.trim(), false, isAmendCommit),
|
||||
operationName,
|
||||
);
|
||||
setCommitMessage("");
|
||||
setIsAmendCommit(false);
|
||||
}, [commitMessage, isAmendCommit, handleGitOperation, gitOps]);
|
||||
|
||||
// Amend dialog state (for editing last commit message directly)
|
||||
const [amendDialogOpen, setAmendDialogOpen] = useState(false);
|
||||
const [amendMessage, setAmendMessage] = useState("");
|
||||
@@ -162,16 +142,8 @@ function useChangesDiscardCommitHandlers(
|
||||
showDiscardDialog,
|
||||
setShowDiscardDialog,
|
||||
fileToDiscard,
|
||||
commitDialogOpen,
|
||||
setCommitDialogOpen,
|
||||
commitMessage,
|
||||
setCommitMessage,
|
||||
isAmendCommit,
|
||||
setIsAmendCommit,
|
||||
handleDiscardClick,
|
||||
handleDiscardConfirm,
|
||||
handleOpenCommitDialog,
|
||||
handleCommit,
|
||||
// Amend dialog
|
||||
amendDialogOpen,
|
||||
setAmendDialogOpen,
|
||||
@@ -211,76 +183,12 @@ function useChangesResetHandlers(gitOps: GitOps, handleGitOperation: GitOperatio
|
||||
};
|
||||
}
|
||||
|
||||
function useChangesPRHandlers(
|
||||
gitOps: GitOps,
|
||||
toast: Toast,
|
||||
taskTitle: string | undefined,
|
||||
baseBranch: string | undefined,
|
||||
firstCommitMessage: string | undefined,
|
||||
) {
|
||||
const [prDialogOpen, setPrDialogOpen] = useState(false);
|
||||
const [prTitle, setPrTitle] = useState("");
|
||||
const [prBody, setPrBody] = useState("");
|
||||
const [prDraft, setPrDraft] = useState(true);
|
||||
|
||||
const handleOpenPRDialog = useCallback(() => {
|
||||
setPrTitle(firstCommitMessage || taskTitle || "");
|
||||
setPrBody("");
|
||||
setPrDialogOpen(true);
|
||||
}, [firstCommitMessage, taskTitle]);
|
||||
const handleCreatePR = useCallback(async () => {
|
||||
if (!prTitle.trim()) return;
|
||||
setPrDialogOpen(false);
|
||||
try {
|
||||
const result = await gitOps.createPR(prTitle.trim(), prBody.trim(), baseBranch, prDraft);
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: prDraft ? "Draft PR created" : "PR created",
|
||||
description: result.pr_url || "PR created successfully",
|
||||
variant: "success",
|
||||
});
|
||||
if (result.pr_url) window.open(result.pr_url, "_blank");
|
||||
} else {
|
||||
toast({
|
||||
title: "Create PR failed",
|
||||
description: result.error || "An error occurred",
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: "Create PR failed",
|
||||
description: e instanceof Error ? e.message : "An error occurred",
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
setPrTitle("");
|
||||
setPrBody("");
|
||||
}, [prTitle, prBody, baseBranch, prDraft, gitOps, toast]);
|
||||
|
||||
return {
|
||||
prDialogOpen,
|
||||
setPrDialogOpen,
|
||||
prTitle,
|
||||
setPrTitle,
|
||||
prBody,
|
||||
setPrBody,
|
||||
prDraft,
|
||||
setPrDraft,
|
||||
handleOpenPRDialog,
|
||||
handleCreatePR,
|
||||
};
|
||||
}
|
||||
|
||||
export function useChangesDialogHandlers(
|
||||
gitOps: GitOps,
|
||||
toast: Toast,
|
||||
handleGitOperation: GitOperationFn,
|
||||
prDefaults: { taskTitle?: string; baseBranch?: string; firstCommitMessage?: string },
|
||||
) {
|
||||
const { taskTitle, baseBranch, firstCommitMessage } = prDefaults;
|
||||
const discardCommit = useChangesDiscardCommitHandlers(gitOps, toast, handleGitOperation);
|
||||
const discardAmend = useChangesDiscardAmendHandlers(gitOps, toast, handleGitOperation);
|
||||
const reset = useChangesResetHandlers(gitOps, handleGitOperation);
|
||||
const pr = useChangesPRHandlers(gitOps, toast, taskTitle, baseBranch, firstCommitMessage);
|
||||
return { ...discardCommit, ...reset, ...pr };
|
||||
return { ...discardAmend, ...reset };
|
||||
}
|
||||
|
||||
@@ -9,15 +9,8 @@ import { hashDiff, normalizeDiffContent } from "@/components/review/types";
|
||||
import type { FileInfo } from "@/lib/state/store";
|
||||
import { useToast } from "@/components/toast-provider";
|
||||
import { useIsTaskArchived, ArchivedPanelPlaceholder } from "./task-archived-context";
|
||||
import { useUtilityAgentGenerator } from "@/hooks/use-utility-agent-generator";
|
||||
import { useIsUtilityConfigured } from "@/hooks/use-is-utility-configured";
|
||||
import {
|
||||
DiscardDialog,
|
||||
CommitDialog,
|
||||
PRDialog,
|
||||
AmendDialog,
|
||||
ResetDialog,
|
||||
} from "./changes-panel-dialogs";
|
||||
import { DiscardDialog, AmendDialog, ResetDialog } from "./changes-panel-dialogs";
|
||||
import { useVcsDialogs } from "@/components/vcs/vcs-dialogs";
|
||||
import { ChangesPanelHeader } from "./changes-panel-header";
|
||||
import {
|
||||
FileListSection,
|
||||
@@ -180,6 +173,8 @@ function useChangesPanelStoreData() {
|
||||
return { activeTaskId, activeSessionId, taskTitle, baseBranch, existingPrUrl };
|
||||
}
|
||||
|
||||
type DialogsType = ReturnType<typeof useChangesDialogHandlers> & ReturnType<typeof useVcsDialogs>;
|
||||
|
||||
type ChangesPanelBodyProps = {
|
||||
hasAnything: boolean;
|
||||
hasUnstaged: boolean;
|
||||
@@ -201,7 +196,7 @@ type ChangesPanelBodyProps = {
|
||||
aheadCount: number;
|
||||
isLoading: boolean;
|
||||
loadingOperation: string | null;
|
||||
dialogs: ReturnType<typeof useChangesDialogHandlers>;
|
||||
dialogs: DialogsType;
|
||||
onOpenDiffFile: (path: string) => void;
|
||||
onEditFile: (path: string) => void;
|
||||
onOpenCommitDetail?: (sha: string) => void;
|
||||
@@ -216,35 +211,12 @@ type ChangesPanelBodyProps = {
|
||||
stagedFileCount: number;
|
||||
stagedAdditions: number;
|
||||
stagedDeletions: number;
|
||||
displayBranch: string | null;
|
||||
baseBranch: string | undefined;
|
||||
utilityGen?: ReturnType<typeof useUtilityAgentGenerator>;
|
||||
isUtilityConfigured?: boolean;
|
||||
};
|
||||
|
||||
function ChangesPanelDialogsSection({
|
||||
dialogs,
|
||||
isLoading,
|
||||
stagedFileCount,
|
||||
stagedAdditions,
|
||||
stagedDeletions,
|
||||
displayBranch,
|
||||
baseBranch,
|
||||
lastCommitMessage,
|
||||
utilityGen,
|
||||
isUtilityConfigured,
|
||||
}: Pick<
|
||||
ChangesPanelBodyProps,
|
||||
| "dialogs"
|
||||
| "isLoading"
|
||||
| "stagedFileCount"
|
||||
| "stagedAdditions"
|
||||
| "stagedDeletions"
|
||||
| "displayBranch"
|
||||
| "baseBranch"
|
||||
| "utilityGen"
|
||||
| "isUtilityConfigured"
|
||||
> & { lastCommitMessage?: string | null }) {
|
||||
}: Pick<ChangesPanelBodyProps, "dialogs" | "isLoading">) {
|
||||
return (
|
||||
<>
|
||||
<DiscardDialog
|
||||
@@ -253,46 +225,6 @@ function ChangesPanelDialogsSection({
|
||||
fileToDiscard={dialogs.fileToDiscard}
|
||||
onConfirm={dialogs.handleDiscardConfirm}
|
||||
/>
|
||||
<CommitDialog
|
||||
open={dialogs.commitDialogOpen}
|
||||
onOpenChange={dialogs.setCommitDialogOpen}
|
||||
commitMessage={dialogs.commitMessage}
|
||||
onCommitMessageChange={dialogs.setCommitMessage}
|
||||
onCommit={dialogs.handleCommit}
|
||||
isLoading={isLoading}
|
||||
stagedFileCount={stagedFileCount}
|
||||
stagedAdditions={stagedAdditions}
|
||||
stagedDeletions={stagedDeletions}
|
||||
isAmend={dialogs.isAmendCommit}
|
||||
onAmendChange={dialogs.setIsAmendCommit}
|
||||
lastCommitMessage={lastCommitMessage}
|
||||
onGenerateMessage={
|
||||
isUtilityConfigured && utilityGen
|
||||
? () => utilityGen.generateCommitMessage(dialogs.setCommitMessage)
|
||||
: undefined
|
||||
}
|
||||
isGenerating={utilityGen?.isGeneratingCommitMessage}
|
||||
/>
|
||||
<PRDialog
|
||||
open={dialogs.prDialogOpen}
|
||||
onOpenChange={dialogs.setPrDialogOpen}
|
||||
prTitle={dialogs.prTitle}
|
||||
onPrTitleChange={dialogs.setPrTitle}
|
||||
prBody={dialogs.prBody}
|
||||
onPrBodyChange={dialogs.setPrBody}
|
||||
prDraft={dialogs.prDraft}
|
||||
onPrDraftChange={dialogs.setPrDraft}
|
||||
onCreatePR={dialogs.handleCreatePR}
|
||||
isLoading={isLoading}
|
||||
displayBranch={displayBranch}
|
||||
baseBranch={baseBranch}
|
||||
onGenerateDescription={
|
||||
isUtilityConfigured && utilityGen
|
||||
? () => utilityGen.generatePRDescription(dialogs.setPrBody)
|
||||
: undefined
|
||||
}
|
||||
isGenerating={utilityGen?.isGeneratingPRDescription}
|
||||
/>
|
||||
<AmendDialog
|
||||
open={dialogs.amendDialogOpen}
|
||||
onOpenChange={dialogs.setAmendDialogOpen}
|
||||
@@ -378,7 +310,7 @@ function TimelineLocalChanges(props: TimelineProps) {
|
||||
isLast={!showCommits}
|
||||
actionLabel="Commit"
|
||||
isActionLoading={props.loadingOperation === "commit"}
|
||||
onAction={props.dialogs.handleOpenCommitDialog}
|
||||
onAction={props.dialogs.openCommitDialog}
|
||||
secondaryActionLabel="Unstage all"
|
||||
isSecondaryActionLoading={isBulkOp && props.loadingOperation === "unstage"}
|
||||
onSecondaryAction={props.onUnstageAll}
|
||||
@@ -401,7 +333,7 @@ function TimelineLocalChanges(props: TimelineProps) {
|
||||
)}
|
||||
{showCommits && (
|
||||
<ActionButtonsSection
|
||||
onOpenPRDialog={props.dialogs.handleOpenPRDialog}
|
||||
onOpenPRDialog={props.dialogs.openPRDialog}
|
||||
onPush={props.onPush}
|
||||
isLoading={props.isLoading}
|
||||
loadingOperation={props.loadingOperation}
|
||||
@@ -453,9 +385,6 @@ function ChangesPanelTimeline(props: TimelineProps) {
|
||||
}
|
||||
|
||||
function ChangesPanelBody(props: ChangesPanelBodyProps) {
|
||||
// Get last commit message for amend feature
|
||||
const lastCommitMessage = props.commits?.[0]?.commit_message ?? null;
|
||||
|
||||
return (
|
||||
<PanelBody className="flex flex-col">
|
||||
<div className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden">
|
||||
@@ -466,18 +395,7 @@ function ChangesPanelBody(props: ChangesPanelBodyProps) {
|
||||
totalFileCount={props.totalFileCount}
|
||||
onOpenReview={props.onOpenReview}
|
||||
/>
|
||||
<ChangesPanelDialogsSection
|
||||
dialogs={props.dialogs}
|
||||
isLoading={props.isLoading}
|
||||
stagedFileCount={props.stagedFileCount}
|
||||
stagedAdditions={props.stagedAdditions}
|
||||
stagedDeletions={props.stagedDeletions}
|
||||
displayBranch={props.displayBranch}
|
||||
baseBranch={props.baseBranch}
|
||||
lastCommitMessage={lastCommitMessage}
|
||||
utilityGen={props.utilityGen}
|
||||
isUtilityConfigured={props.isUtilityConfigured}
|
||||
/>
|
||||
<ChangesPanelDialogsSection dialogs={props.dialogs} isLoading={props.isLoading} />
|
||||
</PanelBody>
|
||||
);
|
||||
}
|
||||
@@ -508,13 +426,13 @@ const ChangesPanel = memo(function ChangesPanel({
|
||||
onOpenReview,
|
||||
}: ChangesPanelProps) {
|
||||
const isArchived = useIsTaskArchived();
|
||||
const { activeTaskId, activeSessionId, taskTitle, baseBranch, existingPrUrl } =
|
||||
useChangesPanelStoreData();
|
||||
const { activeTaskId, activeSessionId, baseBranch, existingPrUrl } = useChangesPanelStoreData();
|
||||
|
||||
const git = useSessionGit(activeSessionId);
|
||||
const { toast } = useToast();
|
||||
const { reviews } = useSessionFileReviews(activeSessionId);
|
||||
const { prDiffFiles, prCommitsList, hasPRFiles, hasPRCommits, prFiles } = useChangesPanelPRData();
|
||||
const vcsDialogs = useVcsDialogs();
|
||||
|
||||
// Periodically check for a PR when the session has a branch but no PR yet
|
||||
useTaskPRDetection(
|
||||
@@ -534,14 +452,8 @@ const ChangesPanel = memo(function ChangesPanel({
|
||||
const staged = useMemo(() => computeStagedStats(git.stagedFiles), [git.stagedFiles]);
|
||||
|
||||
const gitHandlers = useChangesGitHandlers(git, toast, baseBranch);
|
||||
const firstCommitMessage = git.commits?.[0]?.commit_message;
|
||||
const dialogs = useChangesDialogHandlers(git, toast, gitHandlers.handleGitOperation, {
|
||||
taskTitle,
|
||||
baseBranch,
|
||||
firstCommitMessage,
|
||||
});
|
||||
const isUtilityConfigured = useIsUtilityConfigured();
|
||||
const utilityGen = useUtilityAgentGenerator({ sessionId: activeSessionId ?? null, taskTitle });
|
||||
const localDialogs = useChangesDialogHandlers(git, toast, gitHandlers.handleGitOperation);
|
||||
const dialogs = { ...localDialogs, ...vcsDialogs };
|
||||
|
||||
if (isArchived) return <ArchivedPanelPlaceholder />;
|
||||
|
||||
@@ -597,10 +509,6 @@ const ChangesPanel = memo(function ChangesPanel({
|
||||
stagedFileCount={staged.stagedFileCount}
|
||||
stagedAdditions={staged.stagedAdditions}
|
||||
stagedDeletions={staged.stagedDeletions}
|
||||
displayBranch={git.branch}
|
||||
baseBranch={baseBranch}
|
||||
utilityGen={utilityGen}
|
||||
isUtilityConfigured={isUtilityConfigured}
|
||||
/>
|
||||
</PanelRoot>
|
||||
);
|
||||
|
||||
@@ -47,6 +47,7 @@ export type ChatInputEditorAreaProps = {
|
||||
onImplementPlan?: () => void;
|
||||
onEnhancePrompt?: () => void;
|
||||
isEnhancingPrompt?: boolean;
|
||||
isUtilityConfigured?: boolean;
|
||||
};
|
||||
|
||||
function EditorWithTooltip({
|
||||
@@ -143,15 +144,12 @@ export function ChatInputEditorArea({
|
||||
onImplementPlan,
|
||||
onEnhancePrompt,
|
||||
isEnhancingPrompt,
|
||||
isUtilityConfigured,
|
||||
hideSessionsDropdown,
|
||||
}: ChatInputEditorAreaProps) {
|
||||
// Block submit while enhancing prompt, but keep editor editable for programmatic updates
|
||||
const wrappedSubmit = isEnhancingPrompt ? () => {} : handleSubmitWithReset;
|
||||
|
||||
const handleAttachFiles = useCallback(() => {
|
||||
fileInputRef.current?.click();
|
||||
}, [fileInputRef]);
|
||||
|
||||
const handleAttachFiles = useCallback(() => fileInputRef.current?.click(), [fileInputRef]);
|
||||
return (
|
||||
<div className="flex flex-col flex-1 min-h-0 overflow-hidden">
|
||||
<EditorWithTooltip
|
||||
@@ -204,6 +202,7 @@ export function ChatInputEditorArea({
|
||||
onImplementPlan={onImplementPlan}
|
||||
onEnhancePrompt={onEnhancePrompt}
|
||||
isEnhancingPrompt={isEnhancingPrompt}
|
||||
isUtilityConfigured={isUtilityConfigured}
|
||||
onAttachFiles={handleAttachFiles}
|
||||
hideSessionsDropdown={hideSessionsDropdown}
|
||||
/>
|
||||
|
||||
@@ -139,6 +139,7 @@ function buildContextAreaProps(
|
||||
type EnhancePromptExtras = {
|
||||
onEnhancePrompt?: () => void;
|
||||
isEnhancingPrompt?: boolean;
|
||||
isUtilityConfigured?: boolean;
|
||||
};
|
||||
|
||||
function buildEditorAreaProps(
|
||||
@@ -181,6 +182,7 @@ function buildEditorAreaProps(
|
||||
onImplementPlan: p.onImplementPlan,
|
||||
onEnhancePrompt: extras.onEnhancePrompt,
|
||||
isEnhancingPrompt: extras.isEnhancingPrompt,
|
||||
isUtilityConfigured: extras.isUtilityConfigured,
|
||||
hideSessionsDropdown: p.hideSessionsDropdown,
|
||||
};
|
||||
}
|
||||
@@ -273,11 +275,11 @@ export const ChatInputContainer = forwardRef<ChatInputContainerHandle, ChatInput
|
||||
needsRecovery={props.needsRecovery ?? false}
|
||||
addFiles={s.addFiles}
|
||||
contextAreaProps={buildContextAreaProps(s, p)}
|
||||
editorAreaProps={buildEditorAreaProps(
|
||||
s,
|
||||
p,
|
||||
isUtilityConfigured ? { onEnhancePrompt: handleEnhancePrompt, isEnhancingPrompt } : {},
|
||||
)}
|
||||
editorAreaProps={buildEditorAreaProps(s, p, {
|
||||
onEnhancePrompt: handleEnhancePrompt,
|
||||
isEnhancingPrompt,
|
||||
isUtilityConfigured,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -65,6 +65,8 @@ export type ChatInputToolbarProps = {
|
||||
onEnhancePrompt?: () => void;
|
||||
/** Whether prompt enhancement is in progress */
|
||||
isEnhancingPrompt?: boolean;
|
||||
/** Whether utility agent is configured for AI enhancement */
|
||||
isUtilityConfigured?: boolean;
|
||||
/** Callback to open file picker for attaching files */
|
||||
onAttachFiles?: () => void;
|
||||
/** Hide the sessions dropdown (for quick chat) */
|
||||
@@ -181,24 +183,40 @@ function ImplementPlanButton({ onClick }: { onClick: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
function EnhancePromptButton({ onClick, isLoading }: { onClick: () => void; isLoading: boolean }) {
|
||||
function EnhancePromptButton({
|
||||
onClick,
|
||||
isLoading,
|
||||
isConfigured = true,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
isLoading: boolean;
|
||||
isConfigured?: boolean;
|
||||
}) {
|
||||
const isDisabled = !isConfigured || isLoading;
|
||||
const tooltipText = isConfigured
|
||||
? "Enhance prompt with AI"
|
||||
: "Configure a utility agent in settings to enable AI enhancement";
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 cursor-pointer hover:bg-muted/40 text-slate-400"
|
||||
onClick={onClick}
|
||||
disabled={isLoading}
|
||||
aria-label="Enhance prompt with AI"
|
||||
aria-busy={isLoading}
|
||||
>
|
||||
{isLoading ? <GridSpinner className="h-4 w-4" /> : <IconSparkles className="h-4 w-4" />}
|
||||
</Button>
|
||||
{/* Wrap in span so tooltip works even when button is disabled */}
|
||||
<span className="inline-flex">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 cursor-pointer hover:bg-muted/40 text-slate-400"
|
||||
onClick={isConfigured ? onClick : undefined}
|
||||
disabled={isDisabled}
|
||||
aria-label="Enhance prompt with AI"
|
||||
aria-busy={isLoading}
|
||||
>
|
||||
{isLoading ? <GridSpinner className="h-4 w-4" /> : <IconSparkles className="h-4 w-4" />}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Enhance prompt with AI</TooltipContent>
|
||||
<TooltipContent>{tooltipText}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -308,6 +326,7 @@ type ToolbarRightSectionProps = {
|
||||
submitShortcut: (typeof SHORTCUTS)[keyof typeof SHORTCUTS];
|
||||
onEnhancePrompt?: () => void;
|
||||
isEnhancingPrompt?: boolean;
|
||||
isUtilityConfigured?: boolean;
|
||||
onImplementPlan?: () => void;
|
||||
hideSessionsDropdown?: boolean;
|
||||
};
|
||||
@@ -326,10 +345,11 @@ function ToolbarRightSection({
|
||||
submitShortcut,
|
||||
onEnhancePrompt,
|
||||
isEnhancingPrompt,
|
||||
isUtilityConfigured,
|
||||
onImplementPlan,
|
||||
hideSessionsDropdown,
|
||||
}: ToolbarRightSectionProps) {
|
||||
const showEnhance = onEnhancePrompt && !isAgentBusy;
|
||||
const showEnhance = !isAgentBusy;
|
||||
const showImplement = planModeEnabled && !isAgentBusy && onImplementPlan;
|
||||
const showResetContext = !!sessionId && !isAgentBusy;
|
||||
return (
|
||||
@@ -349,7 +369,11 @@ function ToolbarRightSection({
|
||||
<ConfigOptionsDisplay sessionId={sessionId} />
|
||||
{/* AuthMethodsIndicator hidden — UX needs rethinking */}
|
||||
{showEnhance && (
|
||||
<EnhancePromptButton onClick={onEnhancePrompt} isLoading={isEnhancingPrompt ?? false} />
|
||||
<EnhancePromptButton
|
||||
onClick={onEnhancePrompt ?? (() => {})}
|
||||
isLoading={isEnhancingPrompt ?? false}
|
||||
isConfigured={isUtilityConfigured}
|
||||
/>
|
||||
)}
|
||||
{showImplement && <ImplementPlanButton onClick={onImplementPlan} />}
|
||||
<div className="ml-1">
|
||||
@@ -391,6 +415,7 @@ export const ChatInputToolbar = memo(function ChatInputToolbar({
|
||||
onImplementPlan,
|
||||
onEnhancePrompt,
|
||||
isEnhancingPrompt = false,
|
||||
isUtilityConfigured = false,
|
||||
onAttachFiles,
|
||||
hideSessionsDropdown,
|
||||
}: ChatInputToolbarProps) {
|
||||
@@ -465,6 +490,7 @@ export const ChatInputToolbar = memo(function ChatInputToolbar({
|
||||
submitShortcut={submitShortcut}
|
||||
onEnhancePrompt={onEnhancePrompt}
|
||||
isEnhancingPrompt={isEnhancingPrompt}
|
||||
isUtilityConfigured={isUtilityConfigured}
|
||||
onImplementPlan={onImplementPlan}
|
||||
hideSessionsDropdown={hideSessionsDropdown}
|
||||
/>
|
||||
|
||||
@@ -88,6 +88,8 @@ type GenerateButtonProps = {
|
||||
isGenerating: boolean;
|
||||
disabled?: boolean;
|
||||
tooltip: string;
|
||||
notConfiguredTooltip?: string;
|
||||
isConfigured?: boolean;
|
||||
size?: "icon" | "sm";
|
||||
showLabel?: boolean;
|
||||
};
|
||||
@@ -97,31 +99,39 @@ function GenerateButton({
|
||||
isGenerating,
|
||||
disabled,
|
||||
tooltip,
|
||||
notConfiguredTooltip = "Configure a utility agent in settings to enable AI generation",
|
||||
isConfigured = true,
|
||||
size = "icon",
|
||||
showLabel,
|
||||
}: GenerateButtonProps) {
|
||||
const isDisabled = !isConfigured || disabled || isGenerating;
|
||||
const tooltipText = isConfigured ? tooltip : notConfiguredTooltip;
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size={size}
|
||||
variant="ghost"
|
||||
className={
|
||||
size === "icon" ? "h-7 w-7 cursor-pointer" : "h-6 px-2 cursor-pointer gap-1.5 text-xs"
|
||||
}
|
||||
onClick={onClick}
|
||||
disabled={disabled || isGenerating}
|
||||
>
|
||||
{isGenerating ? (
|
||||
<IconLoader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<IconSparkles className="h-4 w-4" />
|
||||
)}
|
||||
{showLabel && "Generate"}
|
||||
</Button>
|
||||
{/* Wrap in span so tooltip works even when button is disabled */}
|
||||
<span className="inline-flex">
|
||||
<Button
|
||||
type="button"
|
||||
size={size}
|
||||
variant="ghost"
|
||||
className={
|
||||
size === "icon" ? "h-7 w-7 cursor-pointer" : "h-6 px-2 cursor-pointer gap-1.5 text-xs"
|
||||
}
|
||||
onClick={isConfigured ? onClick : undefined}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{isGenerating ? (
|
||||
<IconLoader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<IconSparkles className="h-4 w-4" />
|
||||
)}
|
||||
{showLabel && "Generate"}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
<TooltipContent>{tooltipText}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -136,8 +146,9 @@ type CommitDialogProps = {
|
||||
onStageAllChange: (v: boolean) => void;
|
||||
isGitLoading: boolean;
|
||||
onCommit: () => void;
|
||||
onGenerateMessage?: () => void;
|
||||
isGenerating?: boolean;
|
||||
onGenerateMessage: () => void;
|
||||
isGenerating: boolean;
|
||||
isUtilityConfigured: boolean;
|
||||
};
|
||||
|
||||
function CommitDialog({
|
||||
@@ -152,6 +163,7 @@ function CommitDialog({
|
||||
onCommit,
|
||||
onGenerateMessage,
|
||||
isGenerating,
|
||||
isUtilityConfigured,
|
||||
}: CommitDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -174,16 +186,15 @@ function CommitDialog({
|
||||
className="pr-10"
|
||||
autoFocus
|
||||
/>
|
||||
{onGenerateMessage && (
|
||||
<div className="absolute right-1.5 top-1/2 -translate-y-1/2">
|
||||
<GenerateButton
|
||||
onClick={onGenerateMessage}
|
||||
isGenerating={isGenerating ?? false}
|
||||
disabled={fileSummary.count === 0}
|
||||
tooltip="Generate commit message with AI"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute right-1.5 top-1/2 -translate-y-1/2">
|
||||
<GenerateButton
|
||||
onClick={onGenerateMessage}
|
||||
isGenerating={isGenerating}
|
||||
disabled={fileSummary.count === 0}
|
||||
tooltip="Generate commit message with AI"
|
||||
isConfigured={isUtilityConfigured}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
@@ -198,7 +209,7 @@ function CommitDialog({
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline">
|
||||
<Button type="button" variant="outline" className="cursor-pointer">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
@@ -234,8 +245,9 @@ type PRDialogProps = {
|
||||
onPrDraftChange: (v: boolean) => void;
|
||||
isGitLoading: boolean;
|
||||
onCreatePR: () => void;
|
||||
onGenerateDescription?: () => void;
|
||||
isGenerating?: boolean;
|
||||
onGenerateDescription: () => void;
|
||||
isGenerating: boolean;
|
||||
isUtilityConfigured: boolean;
|
||||
};
|
||||
|
||||
function PRBranchSummary({
|
||||
@@ -278,6 +290,7 @@ function PRDialog({
|
||||
onCreatePR,
|
||||
onGenerateDescription,
|
||||
isGenerating,
|
||||
isUtilityConfigured,
|
||||
}: PRDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -307,15 +320,14 @@ function PRDialog({
|
||||
<Label htmlFor="vcs-pr-body" className="text-sm">
|
||||
Description
|
||||
</Label>
|
||||
{onGenerateDescription && (
|
||||
<GenerateButton
|
||||
onClick={onGenerateDescription}
|
||||
isGenerating={isGenerating ?? false}
|
||||
tooltip="Generate PR description with AI"
|
||||
size="sm"
|
||||
showLabel
|
||||
/>
|
||||
)}
|
||||
<GenerateButton
|
||||
onClick={onGenerateDescription}
|
||||
isGenerating={isGenerating}
|
||||
tooltip="Generate PR description with AI"
|
||||
isConfigured={isUtilityConfigured}
|
||||
size="sm"
|
||||
showLabel
|
||||
/>
|
||||
</div>
|
||||
<Textarea
|
||||
id="vcs-pr-body"
|
||||
@@ -339,7 +351,7 @@ function PRDialog({
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline">
|
||||
<Button type="button" variant="outline" className="cursor-pointer">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
@@ -499,10 +511,9 @@ export function VcsDialogsProvider({
|
||||
onStageAllChange={cs.setStageAll}
|
||||
isGitLoading={isGitLoading}
|
||||
onCommit={handleCommit}
|
||||
onGenerateMessage={
|
||||
isUtilityConfigured ? () => generateCommitMessage(cs.setMessage) : undefined
|
||||
}
|
||||
isGenerating={isUtilityConfigured ? isGeneratingCommitMessage : undefined}
|
||||
onGenerateMessage={() => generateCommitMessage(cs.setMessage)}
|
||||
isGenerating={isGeneratingCommitMessage}
|
||||
isUtilityConfigured={isUtilityConfigured}
|
||||
/>
|
||||
<PRDialog
|
||||
open={ps.open}
|
||||
@@ -517,10 +528,9 @@ export function VcsDialogsProvider({
|
||||
onPrDraftChange={ps.setDraft}
|
||||
isGitLoading={isGitLoading}
|
||||
onCreatePR={handleCreatePR}
|
||||
onGenerateDescription={
|
||||
isUtilityConfigured ? () => generatePRDescription(ps.setBody) : undefined
|
||||
}
|
||||
isGenerating={isUtilityConfigured ? isGeneratingPRDescription : undefined}
|
||||
onGenerateDescription={() => generatePRDescription(ps.setBody)}
|
||||
isGenerating={isGeneratingPRDescription}
|
||||
isUtilityConfigured={isUtilityConfigured}
|
||||
/>
|
||||
</VcsDialogsContext.Provider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user