@@ -15,6 +15,7 @@ import { configuration } from '../system/-webview/configuration';
1515import type { Storage } from '../system/-webview/storage' ;
1616import { supportedInVSCodeVersion } from '../system/-webview/vscode' ;
1717import { formatNumeric } from '../system/date' ;
18+ import type { Lazy } from '../system/lazy' ;
1819import type { Deferred } from '../system/promise' ;
1920import { getSettledValue } from '../system/promise' ;
2021import { getPossessiveForm } from '../system/string' ;
@@ -33,6 +34,11 @@ export interface AIResult {
3334 body : string ;
3435}
3536
37+ export interface AIGenerateChangelogChange {
38+ message : string ;
39+ issues : { id : string ; url : string ; title : string | undefined } [ ] ;
40+ }
41+
3642export interface AIModel < Provider extends AIProviders = AIProviders , Model extends string = string > {
3743 readonly id : Model ;
3844 readonly name : string ;
@@ -95,6 +101,12 @@ export interface AIProvider<Provider extends AIProviders = AIProviders> extends
95101 reporting : TelemetryEvents [ 'ai/generate' ] ,
96102 options ?: { cancellation ?: CancellationToken ; context ?: string ; codeSuggestion ?: boolean } ,
97103 ) : Promise < string | undefined > ;
104+ generateChangelog (
105+ model : AIModel < Provider > ,
106+ changes : AIGenerateChangelogChange [ ] ,
107+ reporting : TelemetryEvents [ 'ai/generate' ] ,
108+ options ?: { cancellation ?: CancellationToken } ,
109+ ) : Promise < string | undefined > ;
98110}
99111
100112export class AIProviderService implements Disposable {
@@ -231,7 +243,7 @@ export class AIProviderService implements Disposable {
231243 const changes : string | undefined = await this . getChanges ( changesOrRepo ) ;
232244 if ( changes == null ) return undefined ;
233245
234- const { confirmed, model } = await getModelAndConfirmAIProviderToS ( this , this . container . storage ) ;
246+ const { confirmed, model } = await getModelAndConfirmAIProviderToS ( 'diff' , this , this . container . storage ) ;
235247 if ( model == null ) {
236248 options ?. generating ?. cancel ( ) ;
237249 return undefined ;
@@ -315,7 +327,7 @@ export class AIProviderService implements Disposable {
315327 const changes : string | undefined = await this . getChanges ( changesOrRepo ) ;
316328 if ( changes == null ) return undefined ;
317329
318- const { confirmed, model } = await getModelAndConfirmAIProviderToS ( this , this . container . storage ) ;
330+ const { confirmed, model } = await getModelAndConfirmAIProviderToS ( 'diff' , this , this . container . storage ) ;
319331 if ( model == null ) {
320332 options ?. generating ?. cancel ( ) ;
321333 return undefined ;
@@ -400,7 +412,7 @@ export class AIProviderService implements Disposable {
400412 return undefined ;
401413 }
402414
403- const { confirmed, model } = await getModelAndConfirmAIProviderToS ( this , this . container . storage ) ;
415+ const { confirmed, model } = await getModelAndConfirmAIProviderToS ( 'diff' , this , this . container . storage ) ;
404416 if ( model == null ) {
405417 options ?. generating ?. cancel ( ) ;
406418 return undefined ;
@@ -470,6 +482,73 @@ export class AIProviderService implements Disposable {
470482 }
471483 }
472484
485+ async generateChangelog (
486+ changes : Lazy < Promise < AIGenerateChangelogChange [ ] > > ,
487+ sourceContext : { source : Sources } ,
488+ options ?: { cancellation ?: CancellationToken ; progress ?: ProgressOptions } ,
489+ ) : Promise < string | undefined > {
490+ const { confirmed, model } = await getModelAndConfirmAIProviderToS ( 'data' , this , this . container . storage ) ;
491+ if ( model == null ) return undefined ;
492+
493+ const payload : TelemetryEvents [ 'ai/generate' ] = {
494+ type : 'changelog' ,
495+ 'model.id' : model . id ,
496+ 'model.provider.id' : model . provider . id ,
497+ 'model.provider.name' : model . provider . name ,
498+ 'retry.count' : 0 ,
499+ } ;
500+ const source : Parameters < TelemetryService [ 'sendEvent' ] > [ 2 ] = { source : sourceContext . source } ;
501+
502+ if ( ! confirmed ) {
503+ this . container . telemetry . sendEvent ( 'ai/generate' , { ...payload , 'failed.reason' : 'user-declined' } , source ) ;
504+ return undefined ;
505+ }
506+
507+ if ( options ?. cancellation ?. isCancellationRequested ) {
508+ this . container . telemetry . sendEvent (
509+ 'ai/generate' ,
510+ { ...payload , 'failed.reason' : 'user-cancelled' } ,
511+ source ,
512+ ) ;
513+ return undefined ;
514+ }
515+
516+ const promise = changes . value . then ( changes =>
517+ this . _provider ! . generateChangelog ( model , changes , payload , {
518+ cancellation : options ?. cancellation ,
519+ } ) ,
520+ ) ;
521+
522+ const start = Date . now ( ) ;
523+ try {
524+ const result = await ( options ?. progress != null
525+ ? window . withProgress (
526+ { ...options . progress , title : `Generating changelog with ${ model . name } ...` } ,
527+ ( ) => promise ,
528+ )
529+ : promise ) ;
530+
531+ payload [ 'output.length' ] = result ?. length ;
532+ this . container . telemetry . sendEvent ( 'ai/generate' , { ...payload , duration : Date . now ( ) - start } , source ) ;
533+
534+ return result ;
535+ } catch ( ex ) {
536+ this . container . telemetry . sendEvent (
537+ 'ai/generate' ,
538+ {
539+ ...payload ,
540+ duration : Date . now ( ) - start ,
541+ ...( ex instanceof CancellationError
542+ ? { 'failed.reason' : 'user-cancelled' }
543+ : { 'failed.reason' : 'error' , 'failed.error' : String ( ex ) } ) ,
544+ } ,
545+ source ,
546+ ) ;
547+
548+ throw ex ;
549+ }
550+ }
551+
473552 private async getChanges (
474553 changesOrRepo : string | string [ ] | Repository ,
475554 options ?: { cancellation ?: CancellationToken ; context ?: string ; progress ?: ProgressOptions } ,
@@ -501,7 +580,7 @@ export class AIProviderService implements Disposable {
501580 const diff = await this . container . git . getDiff ( commitOrRevision . repoPath , commitOrRevision . ref ) ;
502581 if ( ! diff ?. contents ) throw new Error ( 'No changes found to explain.' ) ;
503582
504- const { confirmed, model } = await getModelAndConfirmAIProviderToS ( this , this . container . storage ) ;
583+ const { confirmed, model } = await getModelAndConfirmAIProviderToS ( 'diff' , this , this . container . storage ) ;
505584 if ( model == null ) return undefined ;
506585
507586 const payload : TelemetryEvents [ 'ai/explain' ] = {
@@ -632,6 +711,7 @@ export class AIProviderService implements Disposable {
632711}
633712
634713async function getModelAndConfirmAIProviderToS (
714+ confirmationType : 'data' | 'diff' ,
635715 service : AIProviderService ,
636716 storage : Storage ,
637717) : Promise < { confirmed : boolean ; model : AIModel | undefined } > {
@@ -651,7 +731,11 @@ async function getModelAndConfirmAIProviderToS(
651731 const decline : MessageItem = { title : 'Cancel' , isCloseAffordance : true } ;
652732
653733 const result = await window . showInformationMessage (
654- `GitLens AI features require sending a diff of the code changes to ${ model . provider . name } for analysis. This may contain sensitive information.\n\nDo you want to continue?` ,
734+ `GitLens AI features require sending ${
735+ confirmationType === 'data' ? 'data' : 'a diff of the code changes'
736+ } to ${
737+ model . provider . name
738+ } for analysis. This may contain sensitive information.\n\nDo you want to continue?`,
655739 { modal : true } ,
656740 accept ,
657741 switchModel ,
@@ -796,6 +880,14 @@ export function showDiffTruncationWarning(maxCodeCharacters: number, model: AIMo
796880 ) ;
797881}
798882
883+ export function showPromptTruncationWarning ( maxCodeCharacters : number , model : AIModel ) : void {
884+ void window . showWarningMessage (
885+ `The prompt had to be truncated to ${ formatNumeric (
886+ maxCodeCharacters ,
887+ ) } characters to fit within the ${ getPossessiveForm ( model . provider . name ) } limits.`,
888+ ) ;
889+ }
890+
799891export function getValidatedTemperature ( modelTemperature ?: number | null ) : number | undefined {
800892 if ( modelTemperature === null ) return undefined ;
801893 if ( modelTemperature != null ) return modelTemperature ;
0 commit comments