From 7d900fea91c3afbd028f658aee421f0c90cef0b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Almeida?= <57680069+zeval@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:24:30 +0000 Subject: [PATCH] feat(vcs): add commit body field and AI generation for commit description and PR title (#465) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../internal/utility/store/builtins.go | 64 ++++++ .../session-mobile-top-bar-git-controls.tsx | 77 +++++-- apps/web/components/vcs/vcs-dialog-fields.tsx | 199 ++++++++++++++++++ apps/web/components/vcs/vcs-dialogs.tsx | 195 ++++++----------- apps/web/e2e/tests/git-commit-body.spec.ts | 175 +++++++++++++++ .../e2e/tests/git-commit-pre-hooks.spec.ts | 8 +- apps/web/hooks/use-utility-agent-generator.ts | 69 ++++-- 7 files changed, 620 insertions(+), 167 deletions(-) create mode 100644 apps/web/components/vcs/vcs-dialog-fields.tsx create mode 100644 apps/web/e2e/tests/git-commit-body.spec.ts diff --git a/apps/backend/internal/utility/store/builtins.go b/apps/backend/internal/utility/store/builtins.go index 0bb744b114..a18bedff01 100644 --- a/apps/backend/internal/utility/store/builtins.go +++ b/apps/backend/internal/utility/store/builtins.go @@ -42,7 +42,9 @@ func (r *sqliteRepository) getBuiltinAgents() []*models.UtilityAgent { now := time.Now().UTC() return []*models.UtilityAgent{ r.builtinCommitMessage(now), + r.builtinCommitDescription(now), r.builtinBranchName(now), + r.builtinPRTitle(now), r.builtinPRDescription(now), r.builtinEnhancePrompt(now), } @@ -77,6 +79,33 @@ Return ONLY the commit message, no explanations or markdown.`, } } +func (r *sqliteRepository) builtinCommitDescription(now time.Time) *models.UtilityAgent { + return &models.UtilityAgent{ + ID: "builtin-commit-description", + Name: "commit-description", + Description: "Generate a detailed commit description explaining the changes", + AgentID: "", // User must configure + Model: "", // User must configure + Builtin: true, + Enabled: false, // Disabled until user configures + CreatedAt: now, + UpdatedAt: now, + Prompt: `You are a commit description generator. You output ONLY bullet points — no preamble, no commentary, no explanation, no conversational text. + +## Staged Changes (git diff --staged): +{{GitDiff}} + +## Rules: +- Output bullet points ONLY (lines starting with "• " or "- ") +- Do NOT include any text before or after the bullet points +- Do NOT include a commit title/subject line +- Do NOT say things like "Here's the description" or "Based on the changes" +- Focus on what changed and why, not which files were modified +- Group related changes together +- 3-8 bullet points, concise but informative`, + } +} + func (r *sqliteRepository) builtinBranchName(now time.Time) *models.UtilityAgent { return &models.UtilityAgent{ ID: "builtin-branch-name", @@ -109,6 +138,41 @@ Example: feature/add-user-authentication`, } } +func (r *sqliteRepository) builtinPRTitle(now time.Time) *models.UtilityAgent { + return &models.UtilityAgent{ + ID: "builtin-pr-title", + Name: "pr-title", + Description: "Generate a PR title based on commits and changes", + AgentID: "", // User must configure + Model: "", // User must configure + Builtin: true, + Enabled: false, // Disabled until user configures + CreatedAt: now, + UpdatedAt: now, + Prompt: `Generate a concise Pull Request title for the following changes. + +## Commits: +{{CommitLog}} + +## Changed Files: +{{ChangedFiles}} + +## Diff Summary: +{{DiffSummary}} + +## Instructions: +1. Follow the Conventional Commits format: (): +2. Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore +3. Scope is optional but recommended (e.g., api, ui, db) +4. Description should be imperative mood ("add" not "added") +5. Keep it under 72 characters +6. Focus on the overall purpose of the PR, not individual commits + +## Output Format: +Return ONLY the PR title, no explanations or markdown.`, + } +} + func (r *sqliteRepository) builtinPRDescription(now time.Time) *models.UtilityAgent { return &models.UtilityAgent{ ID: "builtin-pr-description", diff --git a/apps/web/components/task/mobile/session-mobile-top-bar-git-controls.tsx b/apps/web/components/task/mobile/session-mobile-top-bar-git-controls.tsx index f9f63c556c..c195738547 100644 --- a/apps/web/components/task/mobile/session-mobile-top-bar-git-controls.tsx +++ b/apps/web/components/task/mobile/session-mobile-top-bar-git-controls.tsx @@ -91,6 +91,42 @@ function useGitToast() { ); } +function useCommitDialogForm( + onOpenChange: (open: boolean) => void, + onCommit: (message: string, stageAll: boolean) => void, +) { + const [commitMessage, setCommitMessage] = useState(""); + const [commitBody, setCommitBody] = useState(""); + const [stageAll, setStageAll] = useState(true); + + const handleOpen = (isOpen: boolean) => { + if (isOpen) { + setCommitMessage(""); + setCommitBody(""); + setStageAll(true); + } + onOpenChange(isOpen); + }; + + const handleCommit = () => { + const title = commitMessage.trim(); + const body = commitBody.trim(); + const fullMessage = body ? `${title}\n\n${body}` : title; + onCommit(fullMessage, stageAll); + }; + + return { + commitMessage, + setCommitMessage, + commitBody, + setCommitBody, + stageAll, + setStageAll, + handleOpen, + handleCommit, + }; +} + export function CommitDialog({ open, onOpenChange, @@ -108,19 +144,10 @@ export function CommitDialog({ isGitLoading: boolean; onCommit: (message: string, stageAll: boolean) => void; }) { - const [commitMessage, setCommitMessage] = useState(""); - const [stageAll, setStageAll] = useState(true); - - const handleOpen = (isOpen: boolean) => { - if (isOpen) { - setCommitMessage(""); - setStageAll(true); - } - onOpenChange(isOpen); - }; + const form = useCommitDialogForm(onOpenChange, onCommit); return ( - + @@ -137,16 +164,31 @@ export function CommitDialog({ /> setCommitMessage(e.target.value)} + value={form.commitMessage} + onChange={(e) => form.setCommitMessage(e.target.value)} autoFocus /> +
+ +