Skip to content

Commit abecfa0

Browse files
feat(commit-message): add prompt template and generator service
Turns collected git context into a commit message. Part 2 of 4 for AI commit-message generation; the VS Code wiring that calls this follows. The prompt exposes the context as separate placeholders - `${branch}`, `${recentCommits}`, `${changedFiles}` and `${diff}` - rather than one opaque blob, so a user editing the prompt in Settings -> Prompts can reorder or drop any of them independently. The diff is fenced in explicit markers and labelled as repository content, since it reaches the model verbatim and can contain instruction-like text. `generator.ts` is deliberately free of VS Code: it takes git context and provider settings and returns cleaned text, locating no repository and writing nowhere, so it can be exercised without the extension host. Its tests load no `vscode` mock at all, which is what keeps that honest. An empty response is now a failure rather than a success. A model that answers with nothing, or with an empty code fence, previously produced an empty message that a caller would happily write over whatever the user had already typed. `config.ts` resolves which profile to generate with. The chosen profile is only a preference: a saved id outlives the profile it points at, and a profile can be deleted between reading the state and looking it up, so both cases fall back to the active configuration instead of stopping generation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent f182cd6 commit abecfa0

46 files changed

Lines changed: 562 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/types/src/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ export const globalSettingsSchema = z.object({
235235
customSupportPrompts: customSupportPromptsSchema.optional(),
236236
enhancementApiConfigId: z.string().optional(),
237237
includeTaskHistoryInEnhance: z.boolean().optional(),
238+
commitMessageApiConfigId: z.string().optional(),
238239
historyPreviewCollapsed: z.boolean().optional(),
239240
reasoningBlockCollapsed: z.boolean().optional(),
240241
/**

packages/types/src/vscode-extension-host.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ export type ExtensionState = Pick<
304304
| "customModePrompts"
305305
| "customSupportPrompts"
306306
| "enhancementApiConfigId"
307+
| "commitMessageApiConfigId"
307308
| "customCondensingPrompt"
308309
| "codebaseIndexConfig"
309310
| "codebaseIndexModels"

src/core/webview/ClineProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2461,6 +2461,7 @@ export class ClineProvider
24612461
customModePrompts,
24622462
customSupportPrompts,
24632463
enhancementApiConfigId,
2464+
commitMessageApiConfigId,
24642465
autoApprovalEnabled,
24652466
customModes,
24662467
experiments,
@@ -2619,6 +2620,7 @@ export class ClineProvider
26192620
customModePrompts: customModePrompts ?? {},
26202621
customSupportPrompts: customSupportPrompts ?? {},
26212622
enhancementApiConfigId,
2623+
commitMessageApiConfigId,
26222624
autoApprovalEnabled: autoApprovalEnabled ?? false,
26232625
customModes,
26242626
experiments: experiments ?? experimentDefault,
@@ -2852,6 +2854,7 @@ export class ClineProvider
28522854
customModePrompts: stateValues.customModePrompts ?? {},
28532855
customSupportPrompts: stateValues.customSupportPrompts ?? {},
28542856
enhancementApiConfigId: stateValues.enhancementApiConfigId,
2857+
commitMessageApiConfigId: stateValues.commitMessageApiConfigId,
28552858
experiments: stateValues.experiments ?? experimentDefault,
28562859
autoApprovalEnabled: stateValues.autoApprovalEnabled ?? false,
28572860
customModes,

src/core/webview/__tests__/ClineProvider.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,6 +1226,47 @@ describe("ClineProvider", () => {
12261226
})
12271227
})
12281228

1229+
describe("commit message model selection is included in state", () => {
1230+
// Both paths matter: the webview reads the posted state to show the current selection, and
1231+
// the generator reads getState() to pick a profile. Dropping either one makes a saved
1232+
// selection look like it reverted.
1233+
it("getStateToPostToWebview returns the saved commitMessageApiConfigId", async () => {
1234+
await provider.resolveWebviewView(mockWebviewView)
1235+
await provider.contextProxy.setValue("commitMessageApiConfigId", "config-2")
1236+
1237+
const state = await provider.getStateToPostToWebview()
1238+
1239+
expect(state.commitMessageApiConfigId).toBe("config-2")
1240+
})
1241+
1242+
it("getStateToPostToWebview leaves commitMessageApiConfigId unset when no profile is chosen", async () => {
1243+
await provider.resolveWebviewView(mockWebviewView)
1244+
await provider.contextProxy.setValue("commitMessageApiConfigId", undefined)
1245+
1246+
const state = await provider.getStateToPostToWebview()
1247+
1248+
expect(state.commitMessageApiConfigId).toBeUndefined()
1249+
})
1250+
1251+
it("getState returns the saved commitMessageApiConfigId", async () => {
1252+
await provider.resolveWebviewView(mockWebviewView)
1253+
await provider.contextProxy.setValue("commitMessageApiConfigId", "config-2")
1254+
1255+
const state = await provider.getState()
1256+
1257+
expect(state.commitMessageApiConfigId).toBe("config-2")
1258+
})
1259+
1260+
it("getState leaves commitMessageApiConfigId unset when no profile is chosen", async () => {
1261+
await provider.resolveWebviewView(mockWebviewView)
1262+
await provider.contextProxy.setValue("commitMessageApiConfigId", undefined)
1263+
1264+
const state = await provider.getState()
1265+
1266+
expect(state.commitMessageApiConfigId).toBeUndefined()
1267+
})
1268+
})
1269+
12291270
it("getStateToPostToWebview passes through defined diffFuzzyThreshold value", async () => {
12301271
await provider.resolveWebviewView(mockWebviewView)
12311272
await provider.contextProxy.setValue("diffFuzzyThreshold", 0.5)

src/i18n/locales/ca/common.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/de/common.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/en/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"update_support_prompt": "Failed to update support prompt",
4141
"reset_support_prompt": "Failed to reset support prompt",
4242
"enhance_prompt": "Failed to enhance prompt",
43+
"commit_message_empty_response": "The model returned an empty commit message.",
4344
"get_system_prompt": "Failed to get system prompt",
4445
"search_commits": "Failed to search commits",
4546
"save_api_config": "Failed to save api configuration",

src/i18n/locales/es/common.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/fr/common.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/hi/common.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)