Files
kandev/apps/web/components/vcs/vcs-dialog-fields.tsx
T
José Almeida 7d900fea91 feat(vcs): add commit body field and AI generation for commit description and PR title (#465)
* 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
2026-03-26 13:24:30 +00:00

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>
);
}