1+ import * as path from "path"
12import * as vscode from "vscode"
23import { ProviderSettingsManager } from "../../core/config/ProviderSettingsManager"
34import { t } from "../../i18n"
@@ -8,13 +9,17 @@ import { CommitMessageGenerator } from "./CommitMessageGenerator"
89import { getCommitMessageGitContextSettings , toGitContextCollectorOptions } from "./gitContextSettings"
910
1011interface VscGenerationRequest {
12+ /** Source control input box that should receive the generated message. */
1113 inputBox : { value : string }
14+ /** Root URI supplied by VS Code for the source control command invocation. */
1215 rootUri ?: vscode . Uri
1316}
1417
18+ /** Registers and handles the VS Code command that writes AI commit messages into SCM input. */
1519export class CommitMessageProvider implements vscode . Disposable {
1620 private generator : CommitMessageGenerator
1721
22+ /** Creates the provider and wires it to the extension settings store. */
1823 constructor (
1924 private context : vscode . ExtensionContext ,
2025 private outputChannel : vscode . OutputChannel ,
@@ -24,6 +29,7 @@ export class CommitMessageProvider implements vscode.Disposable {
2429 this . generator = new CommitMessageGenerator ( providerSettingsManager )
2530 }
2631
32+ /** Registers the generate commit message command with VS Code. */
2733 public async activate ( ) : Promise < void > {
2834 this . outputChannel . appendLine ( t ( "common:commitMessage.activated" ) )
2935
@@ -36,6 +42,7 @@ export class CommitMessageProvider implements vscode.Disposable {
3642 this . context . subscriptions . push ( ...disposables )
3743 }
3844
45+ /** Handles the command invocation from VS Code's SCM UI. */
3946 private async handleVSCodeCommand ( vsRequest ?: VscGenerationRequest ) : Promise < void > {
4047 try {
4148 const workspacePath = this . determineWorkspacePath ( vsRequest ?. rootUri )
@@ -72,7 +79,6 @@ export class CommitMessageProvider implements vscode.Disposable {
7279 vscode . window . showInformationMessage ( t ( "common:commitMessage.noChanges" ) )
7380 return
7481 }
75-
7682 reportProgress ( 25 , t ( "common:commitMessage.foundChanges" , { count : resolution . changes . length } ) )
7783
7884 if ( ! resolution . usedStaged ) {
@@ -94,10 +100,14 @@ export class CommitMessageProvider implements vscode.Disposable {
94100 }
95101
96102 reportProgress ( 70 , t ( "common:commitMessage.generating" ) )
103+ const gitContext = this . appendExistingCommitMessageDraft (
104+ gitContextResult . context ,
105+ targetRepository . inputBox . value ,
106+ )
97107 const message = await this . generator . generateMessage ( {
98108 workspacePath,
99109 selectedFiles : resolution . files ,
100- gitContext : gitContextResult . context ,
110+ gitContext,
101111 onProgress : ( update ) => {
102112 if ( update . percentage !== undefined ) {
103113 reportProgress ( 70 + update . percentage * 0.25 , update . message )
@@ -118,6 +128,7 @@ export class CommitMessageProvider implements vscode.Disposable {
118128 }
119129 }
120130
131+ /** Resolves staged changes, asking before falling back to unstaged worktree changes. */
121132 private async resolveCommitChanges ( gitCollector : GitContextCollector ) : Promise < {
122133 changes : GitChange [ ]
123134 files : string [ ]
@@ -127,6 +138,15 @@ export class CommitMessageProvider implements vscode.Disposable {
127138 let usedStaged = true
128139
129140 if ( changes . length === 0 ) {
141+ const useUnstaged = await this . confirmUnstagedGeneration ( )
142+ if ( ! useUnstaged ) {
143+ return {
144+ changes : [ ] ,
145+ files : [ ] ,
146+ usedStaged,
147+ }
148+ }
149+
130150 changes = await gitCollector . gatherChanges ( { staged : false } )
131151 usedStaged = false
132152 }
@@ -138,6 +158,7 @@ export class CommitMessageProvider implements vscode.Disposable {
138158 }
139159 }
140160
161+ /** Finds the Git repository that owns the requested workspace path. */
141162 private async determineTargetRepository ( workspacePath : string ) : Promise < VscGenerationRequest | null > {
142163 try {
143164 const gitExtension = vscode . extensions . getExtension ( "vscode.git" )
@@ -154,18 +175,31 @@ export class CommitMessageProvider implements vscode.Disposable {
154175 return null
155176 }
156177
157- for ( const repo of gitApi . repositories ?? [ ] ) {
158- if ( repo . rootUri && workspacePath . startsWith ( repo . rootUri . fsPath ) ) {
159- return repo
160- }
178+ const repositories = gitApi . repositories ?? [ ]
179+ const matchingRepositories = repositories
180+ . filter ( ( repo : VscGenerationRequest ) =>
181+ repo . rootUri ? isPathWithinRepository ( workspacePath , repo . rootUri . fsPath ) : false ,
182+ )
183+ . sort (
184+ ( a : VscGenerationRequest , b : VscGenerationRequest ) =>
185+ ( b . rootUri ?. fsPath . length ?? 0 ) - ( a . rootUri ?. fsPath . length ?? 0 ) ,
186+ )
187+
188+ if ( matchingRepositories . length > 0 ) {
189+ return matchingRepositories [ 0 ]
190+ }
191+
192+ if ( repositories . length === 1 ) {
193+ return repositories [ 0 ]
161194 }
162195
163- return gitApi . repositories [ 0 ] ?? null
196+ return null
164197 } catch ( error ) {
165198 return null
166199 }
167200 }
168201
202+ /** Derives the workspace path from the SCM resource or active workspace. */
169203 private determineWorkspacePath ( resourceUri ?: vscode . Uri ) : string {
170204 if ( resourceUri ) {
171205 return resourceUri . fsPath
@@ -179,5 +213,41 @@ export class CommitMessageProvider implements vscode.Disposable {
179213 throw new Error ( "Could not determine workspace path" )
180214 }
181215
216+ /** Adds an existing commit input draft to the model context so the next message can improve it. */
217+ private appendExistingCommitMessageDraft ( gitContext : string , existingDraft : string ) : string {
218+ const normalizedDraft = existingDraft . trim ( )
219+ if ( ! normalizedDraft ) {
220+ return gitContext
221+ }
222+
223+ return `${ gitContext }
224+
225+ ## Existing Commit Message Draft
226+ The Git commit input already contains this draft. Use it as guidance and generate the best final commit message for the changes. You may improve, replace, or preserve parts of it as appropriate.
227+
228+ \`\`\`
229+ ${ normalizedDraft }
230+ \`\`\``
231+ }
232+
233+ /** Confirms whether unstaged changes may be gathered when there are no staged changes. */
234+ private async confirmUnstagedGeneration ( ) : Promise < boolean > {
235+ const confirmAction = t ( "common:commitMessage.confirmUnstagedAction" )
236+ const choice = await vscode . window . showWarningMessage (
237+ t ( "common:commitMessage.useUnstagedConfirm" ) ,
238+ { modal : true } ,
239+ confirmAction ,
240+ )
241+
242+ return choice === confirmAction
243+ }
244+
245+ /** Keeps provider cleanup compatible with VS Code disposable registration. */
182246 public dispose ( ) : void { }
183247}
248+
249+ /** Returns true when the target path is the repository root or is contained by it. */
250+ export function isPathWithinRepository ( targetPath : string , repositoryPath : string ) : boolean {
251+ const relativePath = path . relative ( path . resolve ( repositoryPath ) , path . resolve ( targetPath ) )
252+ return relativePath === "" || ( ! ! relativePath && ! relativePath . startsWith ( ".." ) && ! path . isAbsolute ( relativePath ) )
253+ }
0 commit comments