Skip to content

[2/4] feat(commit-message): add prompt template and generator service - #1228

Open
Rafael-Silva-Oliveira wants to merge 2 commits into
Zoo-Code-Org:mainfrom
Rafael-Silva-Oliveira:feat/commit-msg-2-generator
Open

[2/4] feat(commit-message): add prompt template and generator service#1228
Rafael-Silva-Oliveira wants to merge 2 commits into
Zoo-Code-Org:mainfrom
Rafael-Silva-Oliveira:feat/commit-msg-2-generator

Conversation

@Rafael-Silva-Oliveira

@Rafael-Silva-Oliveira Rafael-Silva-Oliveira commented Aug 12, 2026

Copy link
Copy Markdown

Related GitHub Issue

Closes: #283
Closes: #284
Closes: #285
Closes: #290

Part of: #145 · Stack 2 of 4 · Depends on #1227 · Replaces the all-in-one #1218

Description

Adds the model-facing half of commit-message generation: a customizable prompt, a
dedicated model setting, and the service that turns git context into a message. Still
no command or picker UI, so nothing is reachable by a user yet.

The prompt is editable, for free. COMMIT_MESSAGE is registered in
support-prompt.ts alongside ENHANCE. The Prompts settings tab iterates the
support-prompt registry, so the template immediately gets a textarea, a reset button,
and customSupportPrompts persistence with no new UI code — which is why #290 is
closed here rather than in stack 4.

The default prompt tells the model to account for every changed file. That
instruction is load bearing rather than decorative. Without it, a model given four
staged files across three concerns will confidently describe the largest one and
silently drop the rest. Observed repeatedly in testing before the wording was added.

commitMessageApiConfigId mirrors enhancementApiConfigId exactly, including the
listApiConfigMeta.find(...) guard before getProfile() — that call throws on an
unknown id, so a profile deleted after being selected must fall back to the active
configuration rather than break the button.

Response cleanup. Models wrap answers in code fences and quotes despite being told
not to, so the result is stripped before use.

Progress is reported at ProgressLocation.Window. SourceControl was the obvious
choice but silently drops the title, leaving an unlabelled spinner. No location renders
a cancel button that would do anything useful — see Additional Notes.

Test Procedure

src/services/commit-message/__tests__/generateCommitMessage.spec.ts covers: writing
the cleaned message to the input box, fence/quote stripping, using the dedicated
profile when configured, falling back when the configured profile no longer exists,
selecting the right repository from SourceControl.rootUri in a multi-root workspace,
the no-changes path leaving the input box untouched, the missing-git-extension path,
and generation failures surfacing rather than throwing.

Also asserts progress is reported at a location that actually renders the title, so a
regression back to SourceControl cannot silently produce an unlabelled spinner.

Translations for all 17 locales are included for the new keys, not left as English
fallbacks — find-missing-translations.js gates on this.

Local checks: pnpm lint, pnpm check-types (11/11 packages), full src suite
(7388 passed, 37 skipped), node scripts/find-missing-translations.js.

Pre-Submission Checklist

  • Issue Linked: This PR is linked to an approved GitHub Issue (see "Related GitHub Issue" above).
  • Scope: My changes are focused on the linked issue (one major feature/fix per PR).
  • Self-Review: I have performed a thorough self-review of my code.
  • Testing: New and/or updated tests have been added to cover my changes (if applicable).
  • Visual Snapshot (UI changes only): not applicable — see below.
  • Documentation Impact: I have considered if my changes require documentation updates.
  • Contribution Guidelines: I have read and agree to the Contributor Guidelines.

Visual Snapshots

No new UI components. The Prompts tab gains a "Commit Message" entry, but it is
rendered by the existing support-prompt UI rather than by any code in this PR, so there
is no new surface to snapshot.

Documentation Updates

  • No documentation updates are required.
  • Yes, documentation updates are required.

The editable commit-message prompt and the model setting are both user-facing. Happy to
open a docs PR once the stack is accepted.

Additional Notes

No cancel button, so #289 stays open after this stack. Only
ProgressLocation.Notification renders one, and a toast on every commit would be
intrusive. It would also be inert: completePrompt accepts an abortSignal, but 24 of
25 providers ignore the options argument entirely, so the request cannot actually be
interrupted today. Making cancellation real needs a provider-layer change, which felt
out of scope here — but it is worth knowing that gap exists.

On latency, since it will be the first thing users notice: git collection measures
~120 ms and the prompt is ~600 tokens on a small repo. The wait is the model. A
reasoning-heavy local model spends far longer thinking than the rest of the flow takes,
which is exactly why the dedicated profile setting is in this PR rather than deferred.

Summary by CodeRabbit

  • New Features

    • Generate Conventional Commit messages directly from Source Control changes.
    • Supports staged, unstaged, and untracked changes, with repository and no-change status handling.
    • Allows selecting a dedicated commit-message provider, with fallback to the active provider.
    • Displays generated messages in the repository commit input box with progress feedback.
  • Localization

    • Added commit-message prompts, progress messages, and error states across supported languages.

Rafael-Silva-Oliveira and others added 2 commits August 12, 2026 13:28
Adds `getCommitContext()`, which gathers the changes a commit message should
describe. Part 1 of 4 for AI commit-message generation; nothing consumes it yet.

Staged changes are collected first, since that is what a commit will actually
contain. When nothing is staged it falls back to the working tree so callers
still have something to summarize before staging. The fallback reads
`git status --short` rather than a diff because untracked files appear in no
diff and would otherwise be invisible.

The fallback deliberately runs `git diff` rather than `git diff HEAD`: the index
is known to be empty at that point so the output is identical, but `HEAD` does
not resolve in a repository without an initial commit, where it would fail.

Reuses the existing `checkGitInstalled`, `checkGitRepo`, and `truncateOutput`
helpers. `maxBuffer` is raised past Node's 1MB `exec` default, which real diffs
routinely exceed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Part 2 of 4 for AI commit-message generation. Adds the model-facing half:
a customizable prompt and a service that turns git context into a message.
No command or UI wires it up yet.

- Registers COMMIT_MESSAGE in `support-prompt.ts` alongside ENHANCE. Because
  the Prompts settings tab iterates the support-prompt registry, this gives the
  template an editor and a reset button without any new UI code. The default
  asks for Conventional Commits and tells the model to account for every changed
  file; without that instruction models describe the largest file and silently
  drop the rest.
- Adds `commitMessageApiConfigId` so a small, fast profile can be used for this
  task. Mirrors `enhancementApiConfigId`, including the `listApiConfigMeta`
  guard before `getProfile()` so a deleted profile falls back to the active
  configuration rather than throwing.
- Adds `generateCommitMessage()`, which resolves the repository from the git
  extension API, collects context, and writes the cleaned result into the SCM
  input box. Models wrap answers in fences and quotes despite instruction, so
  the response is stripped before use.

Progress is reported at `ProgressLocation.Window`; `SourceControl` drops the
title, and no location renders a cancel button that would work, since nearly
every provider ignores `completePrompt`'s abort signal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds AI-assisted commit-message generation from staged or working-tree changes. The feature defines a COMMIT_MESSAGE prompt, selects a configured provider with fallback, writes cleaned output to the Source Control input, propagates configuration state, and adds localization and tests.

Changes

Commit-message prompt contract

Layer / File(s) Summary
Prompt definition and localization
src/shared/support-prompt.ts, webview-ui/src/i18n/locales/*/prompts.json
Adds the COMMIT_MESSAGE prompt type and default Conventional Commit instructions. Adds localized labels and descriptions.

Git context collection

Layer / File(s) Summary
Git diff context and validation
src/utils/git.ts, src/utils/__tests__/git.spec.ts
Collects staged changes first, then working-tree changes. Handles untracked files, empty repositories, unavailable Git, clean trees, truncation, and command limits.

Provider configuration state

Layer / File(s) Summary
Commit provider setting propagation
packages/types/src/global-settings.ts, packages/types/src/vscode-extension-host.ts, src/core/webview/ClineProvider.ts
Adds optional commitMessageApiConfigId to global settings and exposes it through extension and webview state.

Source Control integration

Layer / File(s) Summary
Generation service and localized status handling
src/services/commit-message/index.ts, src/services/commit-message/__tests__/generateCommitMessage.spec.ts, src/i18n/locales/*/common.json
Resolves the target repository, selects the dedicated or active provider, generates and cleans the message, updates the repository input box, reports progress and errors, and tests the main paths.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SourceControl
  participant GitExtension
  participant ClineProvider
  participant CompletionHandler
  participant RepositoryInput
  SourceControl->>GitExtension: Resolve clicked or first repository
  GitExtension->>ClineProvider: Retrieve commit context
  ClineProvider->>CompletionHandler: Generate COMMIT_MESSAGE completion
  CompletionHandler->>RepositoryInput: Write cleaned commit message
Loading

Possibly related issues

Possibly related PRs

  • Zoo-Code-Org/Zoo-Code#1218 — Shares the commit-message generation implementation, settings, prompts, Git utilities, tests, and localization changes.

Suggested labels: enhancement, awaiting-review

Suggested reviewers: taltas, hannesrudolph

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed The description includes linked issues, implementation details, test procedures, checklist status, documentation impact, and reviewer notes.
Linked Issues check ✅ Passed The description links four closing issues and identifies related dependencies and replacement context.
Out of Scope Changes check ✅ Passed The changes match the stated model-facing commit-message objectives and exclude the command and picker UI planned for later work.
Title check ✅ Passed The title clearly identifies the commit-message prompt template and generator service added by this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/services/commit-message/index.ts 82.35% 1 Missing and 5 partials ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/utils/__tests__/git.spec.ts (1)

374-378: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the double assertion.

If the overloaded exec type cannot accept implementation directly, add a nearby comment that explains why implementation as unknown as typeof exec is required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/__tests__/git.spec.ts` around lines 374 - 378, Add a nearby comment
at the vitest.mocked(exec).mockImplementation call explaining that the double
assertion is required because exec’s overloaded type cannot accept the test
implementation directly; leave the existing assertion and behavior unchanged.

Source: Coding guidelines

src/services/commit-message/__tests__/generateCommitMessage.spec.ts (1)

44-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document or replace the structural casts.

Lines 53, 66, and 125 hide incomplete ClineProvider and VS Code extension test doubles. Use a precise typed helper where possible. If the casts are required, add nearby comments that state which production members the test intentionally models.

As per coding guidelines: “If an unavoidable cast is required, document why in a nearby comment.”

Also applies to: 61-66, 115-125

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/commit-message/__tests__/generateCommitMessage.spec.ts` around
lines 44 - 53, Update the test doubles around makeProvider and the related VS
Code extension mocks to avoid broad structural casts by using precise typed
helpers where possible. For any unavoidable casts, add nearby comments
identifying the intentionally modeled production members and why the incomplete
ClineProvider or extension shape is required; apply this consistently to the
casts near getState, providerSettingsManager, and the other referenced mocks.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/services/commit-message/__tests__/generateCommitMessage.spec.ts`:
- Around line 102-110: Add a focused test for generateCommitMessage where
listApiConfigMeta includes "config2" but getProfile rejects, asserting the
active apiConfiguration is passed to singleCompletionHandler without showing an
error. Update generateCommitMessage’s profile lookup error handling to fall back
to the active configuration when getProfile fails, while preserving existing
behavior for successful lookups.

In `@src/services/commit-message/index.ts`:
- Around line 101-109: Wrap the getProfile call within the
commitMessageApiConfigId metadata-match block in failure handling so a rejected
lookup leaves the existing configToUse/apiConfiguration fallback unchanged.
Continue assigning providerSettings when the lookup succeeds and apiProvider is
present, and add coverage for stale metadata where getProfile rejects.

---

Nitpick comments:
In `@src/services/commit-message/__tests__/generateCommitMessage.spec.ts`:
- Around line 44-53: Update the test doubles around makeProvider and the related
VS Code extension mocks to avoid broad structural casts by using precise typed
helpers where possible. For any unavoidable casts, add nearby comments
identifying the intentionally modeled production members and why the incomplete
ClineProvider or extension shape is required; apply this consistently to the
casts near getState, providerSettingsManager, and the other referenced mocks.

In `@src/utils/__tests__/git.spec.ts`:
- Around line 374-378: Add a nearby comment at the
vitest.mocked(exec).mockImplementation call explaining that the double assertion
is required because exec’s overloaded type cannot accept the test implementation
directly; leave the existing assertion and behavior unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 568fbd4c-ca02-4a49-a807-018f725793ae

📥 Commits

Reviewing files that changed from the base of the PR and between abaf732 and 1d9a64a.

📒 Files selected for processing (44)
  • packages/types/src/global-settings.ts
  • packages/types/src/vscode-extension-host.ts
  • src/core/webview/ClineProvider.ts
  • src/i18n/locales/ca/common.json
  • src/i18n/locales/de/common.json
  • src/i18n/locales/en/common.json
  • src/i18n/locales/es/common.json
  • src/i18n/locales/fr/common.json
  • src/i18n/locales/hi/common.json
  • src/i18n/locales/id/common.json
  • src/i18n/locales/it/common.json
  • src/i18n/locales/ja/common.json
  • src/i18n/locales/ko/common.json
  • src/i18n/locales/nl/common.json
  • src/i18n/locales/pl/common.json
  • src/i18n/locales/pt-BR/common.json
  • src/i18n/locales/ru/common.json
  • src/i18n/locales/tr/common.json
  • src/i18n/locales/vi/common.json
  • src/i18n/locales/zh-CN/common.json
  • src/i18n/locales/zh-TW/common.json
  • src/services/commit-message/__tests__/generateCommitMessage.spec.ts
  • src/services/commit-message/index.ts
  • src/shared/support-prompt.ts
  • src/utils/__tests__/git.spec.ts
  • src/utils/git.ts
  • webview-ui/src/i18n/locales/ca/prompts.json
  • webview-ui/src/i18n/locales/de/prompts.json
  • webview-ui/src/i18n/locales/en/prompts.json
  • webview-ui/src/i18n/locales/es/prompts.json
  • webview-ui/src/i18n/locales/fr/prompts.json
  • webview-ui/src/i18n/locales/hi/prompts.json
  • webview-ui/src/i18n/locales/id/prompts.json
  • webview-ui/src/i18n/locales/it/prompts.json
  • webview-ui/src/i18n/locales/ja/prompts.json
  • webview-ui/src/i18n/locales/ko/prompts.json
  • webview-ui/src/i18n/locales/nl/prompts.json
  • webview-ui/src/i18n/locales/pl/prompts.json
  • webview-ui/src/i18n/locales/pt-BR/prompts.json
  • webview-ui/src/i18n/locales/ru/prompts.json
  • webview-ui/src/i18n/locales/tr/prompts.json
  • webview-ui/src/i18n/locales/vi/prompts.json
  • webview-ui/src/i18n/locales/zh-CN/prompts.json
  • webview-ui/src/i18n/locales/zh-TW/prompts.json

Comment on lines +102 to +110
it("falls back to the active configuration when the configured profile no longer exists", async () => {
await generateCommitMessage(makeProvider("deleted-config"))

expect(getProfile).not.toHaveBeenCalled()
expect(singleCompletionHandlerModule.singleCompletionHandler).toHaveBeenCalledWith(
apiConfiguration,
expect.any(String),
)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Cover getProfile rejection and preserve the active configuration.

This test only covers an ID absent from listApiConfigMeta. It never calls getProfile.

If listApiConfigMeta contains the ID and getProfile rejects, generateCommitMessage enters its outer catch. It shows an error instead of using apiConfiguration.

Add a test that keeps "config2" in metadata and makes getProfile reject. Update the generator to retain the active configuration when that lookup fails.

As per coding guidelines: “Prefer the narrowest test layer that proves behavior.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/commit-message/__tests__/generateCommitMessage.spec.ts` around
lines 102 - 110, Add a focused test for generateCommitMessage where
listApiConfigMeta includes "config2" but getProfile rejects, asserting the
active apiConfiguration is passed to singleCompletionHandler without showing an
error. Update generateCommitMessage’s profile lookup error handling to fall back
to the active configuration when getProfile fails, while preserving existing
behavior for successful lookups.

Source: Coding guidelines

Comment on lines +101 to +109
if (commitMessageApiConfigId && listApiConfigMeta?.find(({ id }) => id === commitMessageApiConfigId)) {
const { name: _, ...providerSettings } = await provider.providerSettingsManager.getProfile({
id: commitMessageApiConfigId,
})

if (providerSettings.apiProvider) {
configToUse = providerSettings
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the active-profile fallback when getProfile() fails.

Line 102 can throw after the cached metadata check succeeds. This occurs when the profile is deleted or metadata is stale. The outer catch then stops generation instead of using apiConfiguration.

Catch the profile lookup failure in this block and retain configToUse. Add a test where listApiConfigMeta contains the ID but getProfile() rejects.

Proposed fix
 if (commitMessageApiConfigId && listApiConfigMeta?.find(({ id }) => id === commitMessageApiConfigId)) {
-	const { name: _, ...providerSettings } = await provider.providerSettingsManager.getProfile({
-		id: commitMessageApiConfigId,
-	})
-
-	if (providerSettings.apiProvider) {
-		configToUse = providerSettings
+	try {
+		const { name: _, ...providerSettings } = await provider.providerSettingsManager.getProfile({
+			id: commitMessageApiConfigId,
+		})
+
+		if (providerSettings.apiProvider) {
+			configToUse = providerSettings
+		}
+	} catch {
+		// Keep the active configuration when the saved profile is unavailable.
 	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (commitMessageApiConfigId && listApiConfigMeta?.find(({ id }) => id === commitMessageApiConfigId)) {
const { name: _, ...providerSettings } = await provider.providerSettingsManager.getProfile({
id: commitMessageApiConfigId,
})
if (providerSettings.apiProvider) {
configToUse = providerSettings
}
}
if (commitMessageApiConfigId && listApiConfigMeta?.find(({ id }) => id === commitMessageApiConfigId)) {
try {
const { name: _, ...providerSettings } = await provider.providerSettingsManager.getProfile({
id: commitMessageApiConfigId,
})
if (providerSettings.apiProvider) {
configToUse = providerSettings
}
} catch {
// Keep the active configuration when the saved profile is unavailable.
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/commit-message/index.ts` around lines 101 - 109, Wrap the
getProfile call within the commitMessageApiConfigId metadata-match block in
failure handling so a rejected lookup leaves the existing
configToUse/apiConfiguration fallback unchanged. Continue assigning
providerSettings when the lookup succeeds and apiProvider is present, and add
coverage for stale metadata where getProfile rejects.

@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 12, 2026
// one has since been deleted (`getProfile` throws on an unknown id).
let configToUse: ProviderSettings = apiConfiguration

if (commitMessageApiConfigId && listApiConfigMeta?.find(({ id }) => id === commitMessageApiConfigId)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should happen if getProfile() fails because this profile was just deleted? Can we fall back to the active API configuration instead of stopping generation?

* box. Prefers the profile chosen in Settings → Providers → Commit Message Model, falling back to
* the currently active profile.
*/
export async function generateCommitMessage(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to separate the model-facing generator into a function that accepts Git context and provider settings and returns cleaned text? That would keep SCM lookup and UI mutation outside the service as required by issue #284.


Reply with ONLY the commit message - no explanation, no markdown code fences, no surrounding quotes.

\${gitContext}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How will custom prompts independently use the branch, recent commits, changed files, and diff when only ${gitContext} is exposed? Can these be separate fields as required by issue #283?

},
async () => {
const message = await singleCompletionHandler(configToUse, prompt)
repository.inputBox.value = cleanCommitMessage(message)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should an empty cleaned response be treated as an error before this assignment? As written, an empty or fence-only response clears the existing commit message and reports success.

customModePrompts,
customSupportPrompts,
enhancementApiConfigId,
commitMessageApiConfigId,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we cover set and unset commitMessageApiConfigId values through both state-return paths? This would catch a future omission that makes the saved selector revert after a webview refresh.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

2 participants