7d900fea91
* feat(vcs): add commit body/description field to commit dialog
Add an optional description textarea below the commit title in both
desktop and mobile commit dialogs, with a dedicated AI generate button
for the description. Add builtin-commit-description utility agent and
E2E test.
* style: fix prettier formatting in mobile git controls and E2E test
* fix(e2e): use specific testid selectors for commit dialog inputs
The commit dialog now has two textboxes (title + body), so
getByRole("textbox") is ambiguous. Use getByTestId selectors
and add exact match for the Commit button.
* fix(ui): add cursor-pointer to mobile commit button
* fix(ui): remove "(optional)" from description label and "Generate" text from button
* fix(ui): match PR dialog style for commit description generate button
* feat(vcs): add AI generate button for PR title
Add builtin-pr-title utility agent and wire up a generate button
inside the PR title input, matching the commit title pattern.
Extract shared dialog field components into vcs-dialog-fields.tsx
to stay within the 600-line file limit.
* refactor(ui): unify generate buttons as icon-only sparkles inside inputs
Move AI generate buttons inside both title inputs and description
textareas (top-right corner) for a consistent look. Remove the
"Generate" label variant — all buttons are now icon-only sparkles.
* style: fix prettier formatting in utility agent generator hook
* fix(a11y): add aria-labels to generate buttons and PR title input
* ci: retrigger E2E tests
* fix(e2e): improve commit dialog button click reliability
Add exact match and enabled check before clicking the dialog
Commit button, increase close timeout.
* ci: retrigger checks
* fix(e2e): improve pre-hooks test commit button reliability
Add exact match, enabled check, and longer dialog timeout to match
the commit-body test pattern.
* fix(ui): prevent dialog field overflow and improve commit description prompt
- Add min-w-0 to relative wrappers in commit and PR dialog fields to
prevent text inputs/textareas from overflowing the dialog bounds
- Rewrite commit description prompt to use system-role framing that
prevents weaker models from adding conversational preamble
200 lines
5.3 KiB
TypeScript
200 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import { IconLoader2, IconSparkles } from "@tabler/icons-react";
|
|
import { Button } from "@kandev/ui/button";
|
|
import { Label } from "@kandev/ui/label";
|
|
import { Input } from "@kandev/ui/input";
|
|
import { Textarea } from "@kandev/ui/textarea";
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
|
|
|
type GenerateButtonProps = {
|
|
onClick: () => void;
|
|
isGenerating: boolean;
|
|
disabled?: boolean;
|
|
tooltip: string;
|
|
notConfiguredTooltip?: string;
|
|
isConfigured?: boolean;
|
|
};
|
|
|
|
export function GenerateButton({
|
|
onClick,
|
|
isGenerating,
|
|
disabled,
|
|
tooltip,
|
|
notConfiguredTooltip = "Configure a utility agent in settings to enable AI generation",
|
|
isConfigured = true,
|
|
}: GenerateButtonProps) {
|
|
const isDisabled = !isConfigured || disabled || isGenerating;
|
|
const tooltipText = isConfigured ? tooltip : notConfiguredTooltip;
|
|
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span className="inline-flex">
|
|
<Button
|
|
type="button"
|
|
size="icon"
|
|
variant="ghost"
|
|
aria-label={tooltip}
|
|
className="h-7 w-7 cursor-pointer"
|
|
onClick={isConfigured ? onClick : undefined}
|
|
disabled={isDisabled}
|
|
>
|
|
{isGenerating ? (
|
|
<IconLoader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<IconSparkles className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{tooltipText}</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
export function CommitBodyField({
|
|
commitBody,
|
|
onCommitBodyChange,
|
|
onGenerateDescription,
|
|
isGeneratingDescription,
|
|
isUtilityConfigured,
|
|
disabled,
|
|
}: {
|
|
commitBody: string;
|
|
onCommitBodyChange: (v: string) => void;
|
|
onGenerateDescription: () => void;
|
|
isGeneratingDescription: boolean;
|
|
isUtilityConfigured: boolean;
|
|
disabled: boolean;
|
|
}) {
|
|
return (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="vcs-commit-body" className="text-sm">
|
|
Description
|
|
</Label>
|
|
<div className="relative min-w-0">
|
|
<Textarea
|
|
id="vcs-commit-body"
|
|
data-testid="commit-body-input"
|
|
placeholder="Add details about this change..."
|
|
value={commitBody}
|
|
onChange={(e) => onCommitBodyChange(e.target.value)}
|
|
rows={3}
|
|
className="resize-none max-h-[200px] overflow-y-auto pr-10"
|
|
/>
|
|
<div className="absolute right-1.5 top-1.5">
|
|
<GenerateButton
|
|
onClick={onGenerateDescription}
|
|
isGenerating={isGeneratingDescription}
|
|
disabled={disabled}
|
|
tooltip="Generate commit description with AI"
|
|
isConfigured={isUtilityConfigured}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function PRTitleField({
|
|
prTitle,
|
|
onPrTitleChange,
|
|
onGenerateTitle,
|
|
isGeneratingTitle,
|
|
isUtilityConfigured,
|
|
}: {
|
|
prTitle: string;
|
|
onPrTitleChange: (v: string) => void;
|
|
onGenerateTitle: () => void;
|
|
isGeneratingTitle: boolean;
|
|
isUtilityConfigured: boolean;
|
|
}) {
|
|
return (
|
|
<div className="relative min-w-0">
|
|
<Input
|
|
id="vcs-pr-title"
|
|
aria-label="Pull request title"
|
|
placeholder="Pull request title..."
|
|
value={prTitle}
|
|
onChange={(e) => onPrTitleChange(e.target.value)}
|
|
className="pr-10"
|
|
autoFocus
|
|
/>
|
|
<div className="absolute right-1.5 top-1/2 -translate-y-1/2">
|
|
<GenerateButton
|
|
onClick={onGenerateTitle}
|
|
isGenerating={isGeneratingTitle}
|
|
tooltip="Generate PR title with AI"
|
|
isConfigured={isUtilityConfigured}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function PRDescriptionField({
|
|
prBody,
|
|
onPrBodyChange,
|
|
onGenerateDescription,
|
|
isGeneratingDescription,
|
|
isUtilityConfigured,
|
|
}: {
|
|
prBody: string;
|
|
onPrBodyChange: (v: string) => void;
|
|
onGenerateDescription: () => void;
|
|
isGeneratingDescription: boolean;
|
|
isUtilityConfigured: boolean;
|
|
}) {
|
|
return (
|
|
<div className="space-y-2">
|
|
<Label htmlFor="vcs-pr-body" className="text-sm">
|
|
Description
|
|
</Label>
|
|
<div className="relative min-w-0">
|
|
<Textarea
|
|
id="vcs-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"
|
|
/>
|
|
<div className="absolute right-1.5 top-1.5">
|
|
<GenerateButton
|
|
onClick={onGenerateDescription}
|
|
isGenerating={isGeneratingDescription}
|
|
tooltip="Generate PR description with AI"
|
|
isConfigured={isUtilityConfigured}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function PRBranchSummary({
|
|
displayBranch,
|
|
baseBranch,
|
|
}: {
|
|
displayBranch?: string | null;
|
|
baseBranch?: string;
|
|
}) {
|
|
if (!displayBranch) return null;
|
|
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>
|
|
);
|
|
}
|