refactor(web): consolidate commit and PR dialogs to use vcs-dialogs (#426)

This commit is contained in:
João Salavisa
2026-03-12 22:41:53 +00:00
committed by GitHub
parent ae9037f63c
commit 06e0e757e5
7 changed files with 132 additions and 606 deletions
@@ -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>{" "}
&rarr; <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 };
}
+13 -105
View File
@@ -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}
/>
+61 -51
View File
@@ -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>
);