Skip to content

Commit 69aa82d

Browse files
committed
feat: Add AI-powered commit messages
This commit introduces a new experimental feature that uses AI to generate commit messages based on the staged change
1 parent c8c5646 commit 69aa82d

File tree

28 files changed

+205
-26
lines changed

28 files changed

+205
-26
lines changed

packages/types/src/experiment.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js"
66
* ExperimentId
77
*/
88

9-
export const experimentIds = ["powerSteering", "multiFileApplyDiff"] as const
9+
export const experimentIds = ["powerSteering", "multiFileApplyDiff", "aiCommitMessages"] as const
1010

1111
export const experimentIdsSchema = z.enum(experimentIds)
1212

@@ -19,6 +19,7 @@ export type ExperimentId = z.infer<typeof experimentIdsSchema>
1919
export const experimentsSchema = z.object({
2020
powerSteering: z.boolean().optional(),
2121
multiFileApplyDiff: z.boolean().optional(),
22+
aiCommitMessages: z.boolean().optional(),
2223
})
2324

2425
export type Experiments = z.infer<typeof experimentsSchema>

packages/types/src/vscode.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export const commandIds = [
5353
"focusInput",
5454
"acceptInput",
5555
"focusPanel",
56-
5756
"git.generateCommitMessage",
5857
] as const
5958

src/core/webview/webviewMessageHandler.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,6 +1428,19 @@ export const webviewMessageHandler = async (
14281428

14291429
await updateGlobalState("experiments", updatedExperiments)
14301430

1431+
// Also update workspace settings to trigger the context update.
1432+
await vscode.workspace
1433+
.getConfiguration(Package.name)
1434+
.update("experiments", updatedExperiments, vscode.ConfigurationTarget.Global)
1435+
1436+
if (message.values.aiCommitMessages !== undefined) {
1437+
await vscode.commands.executeCommand(
1438+
"setContext",
1439+
"roo-cline.aiCommitMessagesEnabled",
1440+
message.values.aiCommitMessages,
1441+
)
1442+
}
1443+
14311444
await provider.postStateToWebview()
14321445
break
14331446
}

src/extension.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,28 @@ export async function activate(context: vscode.ExtensionContext) {
9999
const contextProxy = await ContextProxy.getInstance(context)
100100
const codeIndexManager = CodeIndexManager.getInstance(context)
101101

102+
const setAiCommitMessagesContext = () => {
103+
const config = vscode.workspace.getConfiguration(Package.name)
104+
const experiments = config.get("experiments", {}) as { aiCommitMessages?: boolean }
105+
vscode.commands.executeCommand(
106+
"setContext",
107+
"roo-cline.aiCommitMessagesEnabled",
108+
!!experiments.aiCommitMessages,
109+
)
110+
}
111+
112+
// Set the initial context
113+
setAiCommitMessagesContext()
114+
115+
// Update the context when the configuration changes
116+
context.subscriptions.push(
117+
vscode.workspace.onDidChangeConfiguration((e) => {
118+
if (e.affectsConfiguration(`${Package.name}.experiments`)) {
119+
setAiCommitMessagesContext()
120+
}
121+
}),
122+
)
123+
102124
try {
103125
await codeIndexManager?.initialize(contextProxy)
104126
} catch (error) {

src/package.json

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@
178178
}
179179
],
180180
"menus": {
181+
"commandPalette": [
182+
{
183+
"command": "roo-cline.git.generateCommitMessage",
184+
"when": "roo-cline.aiCommitMessagesEnabled"
185+
}
186+
],
181187
"editor/context": [
182188
{
183189
"submenu": "roo-cline.contextMenu",
@@ -291,7 +297,7 @@
291297
{
292298
"command": "roo-cline.git.generateCommitMessage",
293299
"group": "navigation",
294-
"when": "scmProvider == git"
300+
"when": "scmProvider == git && roo-cline.aiCommitMessagesEnabled"
295301
}
296302
]
297303
},
@@ -346,6 +352,19 @@
346352
"type": "boolean",
347353
"default": true,
348354
"description": "%settings.enableCodeActions.description%"
355+
},
356+
"roo-cline.experiments": {
357+
"type": "object",
358+
"default": {
359+
"aiCommitMessages": false
360+
},
361+
"properties": {
362+
"aiCommitMessages": {
363+
"type": "boolean",
364+
"default": false,
365+
"description": "Enable AI-powered commit messages"
366+
}
367+
}
349368
}
350369
}
351370
}

src/shared/__tests__/experiments.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe("experiments", () => {
2828
const experiments: Record<ExperimentId, boolean> = {
2929
powerSteering: false,
3030
multiFileApplyDiff: false,
31+
aiCommitMessages: false,
3132
}
3233
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
3334
})
@@ -36,6 +37,7 @@ describe("experiments", () => {
3637
const experiments: Record<ExperimentId, boolean> = {
3738
powerSteering: true,
3839
multiFileApplyDiff: false,
40+
aiCommitMessages: false,
3941
}
4042
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true)
4143
})
@@ -44,6 +46,7 @@ describe("experiments", () => {
4446
const experiments: Record<ExperimentId, boolean> = {
4547
powerSteering: false,
4648
multiFileApplyDiff: false,
49+
aiCommitMessages: false,
4750
}
4851
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
4952
})

src/shared/experiments.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { AssertEqual, Equals, Keys, Values, ExperimentId, Experiments } fro
33
export const EXPERIMENT_IDS = {
44
MULTI_FILE_APPLY_DIFF: "multiFileApplyDiff",
55
POWER_STEERING: "powerSteering",
6+
AI_COMMIT_MESSAGES: "aiCommitMessages",
67
} as const satisfies Record<string, ExperimentId>
78

89
type _AssertExperimentIds = AssertEqual<Equals<ExperimentId, Values<typeof EXPERIMENT_IDS>>>
@@ -16,6 +17,7 @@ interface ExperimentConfig {
1617
export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
1718
MULTI_FILE_APPLY_DIFF: { enabled: false },
1819
POWER_STEERING: { enabled: false },
20+
AI_COMMIT_MESSAGES: { enabled: false },
1921
}
2022

2123
export const experimentDefault = Object.fromEntries(

webview-ui/src/components/settings/LanguageSettings.tsx

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ type LanguageSettingsProps = HTMLAttributes<HTMLDivElement> & {
1717
language: string
1818
commitLanguage: string
1919
setCachedStateField: SetCachedStateField<"language" | "commitLanguage">
20+
aiCommitMessagesEnabled?: boolean
2021
}
2122

2223
export const LanguageSettings = ({
2324
language,
2425
commitLanguage,
2526
setCachedStateField,
27+
aiCommitMessagesEnabled,
2628
className,
2729
...props
2830
}: LanguageSettingsProps) => {
@@ -54,24 +56,30 @@ export const LanguageSettings = ({
5456
</SelectGroup>
5557
</SelectContent>
5658
</Select>
57-
<div className="text-sm text-vscode-descriptionForeground">{t("settings:sections.commitLanguage")}</div>
58-
<Select
59-
value={commitLanguage}
60-
onValueChange={(value) => setCachedStateField("commitLanguage", value as Language)}>
61-
<SelectTrigger className="w-full">
62-
<SelectValue placeholder={t("settings:common.select")} />
63-
</SelectTrigger>
64-
<SelectContent>
65-
<SelectGroup>
66-
{Object.entries(LANGUAGES).map(([code, name]) => (
67-
<SelectItem key={code} value={code}>
68-
{name}
69-
<span className="text-muted-foreground">({code})</span>
70-
</SelectItem>
71-
))}
72-
</SelectGroup>
73-
</SelectContent>
74-
</Select>
59+
{aiCommitMessagesEnabled && (
60+
<>
61+
<div className="text-sm text-vscode-descriptionForeground">
62+
{t("settings:sections.commitLanguage")}
63+
</div>
64+
<Select
65+
value={commitLanguage}
66+
onValueChange={(value) => setCachedStateField("commitLanguage", value as Language)}>
67+
<SelectTrigger className="w-full">
68+
<SelectValue placeholder={t("settings:common.select")} />
69+
</SelectTrigger>
70+
<SelectContent>
71+
<SelectGroup>
72+
{Object.entries(LANGUAGES).map(([code, name]) => (
73+
<SelectItem key={code} value={code}>
74+
{name}
75+
<span className="text-muted-foreground">({code})</span>
76+
</SelectItem>
77+
))}
78+
</SelectGroup>
79+
</SelectContent>
80+
</Select>
81+
</>
82+
)}
7583
</Section>
7684
</div>
7785
)

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
700700
language={language || "en"}
701701
commitLanguage={commitLanguage || "en"}
702702
setCachedStateField={setCachedStateField}
703+
aiCommitMessagesEnabled={experiments?.aiCommitMessages}
703704
/>
704705
)}
705706

webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,42 @@ describe("mergeExtensionState", () => {
208208
cloudIsAuthenticated: false,
209209
sharingEnabled: false,
210210
profileThresholds: {},
211-
hasOpenedModeSelector: false, // Add the new required property
211+
hasOpenedModeSelector: false,
212+
language: "en",
213+
commitLanguage: "en",
214+
soundVolume: 0.5,
215+
ttsEnabled: false,
216+
ttsSpeed: 1.0,
217+
diffEnabled: false,
218+
fuzzyMatchThreshold: 1.0,
219+
browserViewportSize: "900x600",
220+
screenshotQuality: 75,
221+
terminalOutputLineLimit: 500,
222+
terminalShellIntegrationTimeout: 4000,
223+
currentApiConfigName: "default",
224+
listApiConfigMeta: [],
225+
customSupportPrompts: {},
226+
enhancementApiConfigId: "",
227+
condensingApiConfigId: "",
228+
customCondensingPrompt: "",
229+
autoApprovalEnabled: false,
230+
cwd: "",
231+
browserToolEnabled: true,
232+
pinnedApiConfigs: {},
233+
terminalZshOhMy: false,
234+
maxConcurrentFileReads: 5,
235+
terminalZshP10k: false,
236+
terminalZdotdir: false,
237+
terminalCompressProgressBar: true,
238+
historyPreviewCollapsed: false,
239+
codebaseIndexConfig: {
240+
codebaseIndexEnabled: false,
241+
codebaseIndexQdrantUrl: "http://localhost:6333",
242+
codebaseIndexEmbedderProvider: "openai",
243+
codebaseIndexEmbedderBaseUrl: "",
244+
codebaseIndexEmbedderModelId: "",
245+
},
246+
codebaseIndexModels: { ollama: {}, openai: {} },
212247
}
213248

214249
const prevState: ExtensionState = {
@@ -226,22 +261,22 @@ describe("mergeExtensionState", () => {
226261
disableCompletionCommand: false,
227262
concurrentFileReads: true,
228263
multiFileApplyDiff: true,
264+
aiCommitMessages: false,
229265
} as Record<ExperimentId, boolean>,
230266
}
231267

232268
const result = mergeExtensionState(prevState, newState)
233269

234-
expect(result.apiConfiguration).toEqual({
235-
modelMaxThinkingTokens: 456,
236-
modelTemperature: 0.3,
237-
})
270+
// The entire apiConfiguration object should be replaced, not merged
271+
expect(result.apiConfiguration).toEqual(newState.apiConfiguration)
238272

239273
expect(result.experiments).toEqual({
240274
powerSteering: true,
241275
marketplace: false,
242276
disableCompletionCommand: false,
243277
concurrentFileReads: true,
244278
multiFileApplyDiff: true,
279+
aiCommitMessages: false,
245280
})
246281
})
247282
})

0 commit comments

Comments
 (0)