Skip to content

Commit 8fc6ac9

Browse files
Updates AI feature flows (#4172)
* Adds access step to some features * Tweaks messaging from review --------- Co-authored-by: Eric Amodio <[email protected]>
1 parent c283896 commit 8fc6ac9

File tree

7 files changed

+129
-8
lines changed

7 files changed

+129
-8
lines changed

contributions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4906,7 +4906,7 @@
49064906
}
49074907
},
49084908
"gitlens.views.ai.generateChangelog": {
4909-
"label": "Generate Changelog",
4909+
"label": "Generate Changelog (Preview)",
49104910
"icon": "$(sparkle)",
49114911
"menus": {
49124912
"view/item/context": [

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7833,7 +7833,7 @@
78337833
},
78347834
{
78357835
"command": "gitlens.views.ai.generateChangelog",
7836-
"title": "Generate Changelog",
7836+
"title": "Generate Changelog (Preview)",
78377837
"icon": "$(sparkle)"
78387838
},
78397839
{

src/features.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,31 @@ export type RepoFeatureAccess =
3030
visibility?: RepositoryVisibility;
3131
};
3232

33-
export type PlusFeatures = 'timeline' | 'worktrees' | 'graph' | 'launchpad' | 'startWork' | 'associateIssueWithBranch';
33+
export type PlusFeatures = ProFeatures | AdvancedFeatures;
34+
35+
export type ProFeatures =
36+
| 'timeline'
37+
| 'worktrees'
38+
| 'graph'
39+
| 'launchpad'
40+
| 'startWork'
41+
| 'associateIssueWithBranch'
42+
| ProAIFeatures;
43+
export type ProAIFeatures = 'generateStashMessage' | 'explainCommit' | 'cloudPatchGenerateTitleAndDescription';
44+
45+
export type AdvancedFeatures = AdvancedAIFeatures;
46+
export type AdvancedAIFeatures = 'generateChangelog';
47+
48+
export type AIFeatures = ProAIFeatures | AdvancedAIFeatures;
49+
50+
export function isAdvancedFeature(feature: PlusFeatures): feature is AdvancedFeatures {
51+
switch (feature) {
52+
case 'generateChangelog':
53+
return true;
54+
default:
55+
return false;
56+
}
57+
}
3458

3559
export type FeaturePreviews = 'graph';
3660
export const featurePreviews: FeaturePreviews[] = ['graph'];

src/git/gitProviderService.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,15 @@ export class GitProviderService implements Disposable {
775775
return { allowed: subscription.account?.verified !== false, subscription: { current: subscription } };
776776
}
777777

778-
if (feature === 'launchpad' || feature === 'startWork' || feature === 'associateIssueWithBranch') {
778+
if (
779+
feature === 'launchpad' ||
780+
feature === 'startWork' ||
781+
feature === 'associateIssueWithBranch' ||
782+
feature === 'generateStashMessage' ||
783+
feature === 'explainCommit' ||
784+
feature === 'cloudPatchGenerateTitleAndDescription' ||
785+
feature === 'generateChangelog'
786+
) {
779787
return { allowed: false, subscription: { current: subscription, required: SubscriptionPlanId.Pro } };
780788
}
781789

src/plus/ai/aiProviderService.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { primaryAIProviders } from '../../constants.ai';
55
import type { AIGenerateDraftEventData, Source, TelemetryEvents } from '../../constants.telemetry';
66
import type { Container } from '../../container';
77
import { CancellationError } from '../../errors';
8+
import type { AIFeatures } from '../../features';
9+
import { isAdvancedFeature } from '../../features';
810
import type { GitCommit } from '../../git/models/commit';
911
import { isCommit } from '../../git/models/commit';
1012
import type { GitRevisionReference } from '../../git/models/reference';
@@ -13,6 +15,7 @@ import { uncommitted, uncommittedStaged } from '../../git/models/revision';
1315
import { assertsCommitHasFullDetails } from '../../git/utils/commit.utils';
1416
import { showAIModelPicker } from '../../quickpicks/aiModelPicker';
1517
import { configuration } from '../../system/-webview/configuration';
18+
import { getContext } from '../../system/-webview/context';
1619
import type { Storage } from '../../system/-webview/storage';
1720
import { supportedInVSCodeVersion } from '../../system/-webview/vscode';
1821
import { debounce } from '../../system/function/debounce';
@@ -22,6 +25,7 @@ import { lazy } from '../../system/lazy';
2225
import type { Deferred } from '../../system/promise';
2326
import { getSettledValue } from '../../system/promise';
2427
import type { ServerConnection } from '../gk/serverConnection';
28+
import { ensureFeatureAccess } from '../gk/utils/-webview/acount.utils';
2529
import type { AIActionType, AIModel, AIModelDescriptor } from './models/model';
2630
import type { PromptTemplateContext } from './models/promptTemplates';
2731
import type { AIProvider, AIRequestResult } from './models/provider';
@@ -284,11 +288,44 @@ export class AIProviderService implements Disposable {
284288
return model;
285289
}
286290

291+
private async ensureOrgAccess(): Promise<boolean> {
292+
const orgEnabled = getContext('gitlens:gk:organization:ai:enabled');
293+
if (orgEnabled === false) {
294+
await window.showErrorMessage(`AI features have been disabled for your organization.`);
295+
return false;
296+
}
297+
298+
return true;
299+
}
300+
301+
private async ensureFeatureAccess(feature: AIFeatures, source: Source): Promise<boolean> {
302+
if (!(await this.ensureOrgAccess())) return false;
303+
304+
if (
305+
!(await ensureFeatureAccess(
306+
this.container,
307+
isAdvancedFeature(feature)
308+
? `Advanced AI features require a trial or GitLens Advanced.`
309+
: `Pro AI features require a trial or GitLens Pro.`,
310+
feature,
311+
source,
312+
))
313+
) {
314+
return false;
315+
}
316+
317+
return true;
318+
}
319+
287320
async explainCommit(
288321
commitOrRevision: GitRevisionReference | GitCommit,
289322
sourceContext: Source & { type: TelemetryEvents['ai/explain']['changeType'] },
290323
options?: { cancellation?: CancellationToken; progress?: ProgressOptions },
291324
): Promise<AISummarizeResult | undefined> {
325+
if (!(await this.ensureFeatureAccess('explainCommit', sourceContext))) {
326+
return undefined;
327+
}
328+
292329
const diff = await this.container.git.diff(commitOrRevision.repoPath).getDiff?.(commitOrRevision.ref);
293330
if (!diff?.contents) throw new Error('No changes found to explain.');
294331

@@ -339,6 +376,8 @@ export class AIProviderService implements Disposable {
339376
progress?: ProgressOptions;
340377
},
341378
): Promise<AISummarizeResult | undefined> {
379+
if (!(await this.ensureOrgAccess())) return undefined;
380+
342381
const changes: string | undefined = await this.getChanges(changesOrRepo);
343382
if (changes == null) return undefined;
344383

@@ -377,6 +416,10 @@ export class AIProviderService implements Disposable {
377416
codeSuggestion?: boolean;
378417
},
379418
): Promise<AISummarizeResult | undefined> {
419+
if (!(await this.ensureFeatureAccess('cloudPatchGenerateTitleAndDescription', sourceContext))) {
420+
return undefined;
421+
}
422+
380423
const changes: string | undefined = await this.getChanges(changesOrRepo);
381424
if (changes == null) return undefined;
382425

@@ -423,6 +466,10 @@ export class AIProviderService implements Disposable {
423466
progress?: ProgressOptions;
424467
},
425468
): Promise<AISummarizeResult | undefined> {
469+
if (!(await this.ensureFeatureAccess('generateStashMessage', source))) {
470+
return undefined;
471+
}
472+
426473
const changes: string | undefined = await this.getChanges(changesOrRepo);
427474
if (changes == null) {
428475
options?.generating?.cancel();
@@ -458,6 +505,10 @@ export class AIProviderService implements Disposable {
458505
source: Source,
459506
options?: { cancellation?: CancellationToken; progress?: ProgressOptions },
460507
): Promise<AIResult | undefined> {
508+
if (!(await this.ensureFeatureAccess('generateChangelog', source))) {
509+
return undefined;
510+
}
511+
461512
const result = await this.sendRequest(
462513
'generate-changelog',
463514
async () => ({

src/plus/gk/subscriptionService.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -882,10 +882,10 @@ export class SubscriptionService implements Disposable {
882882
}
883883

884884
@log()
885-
async upgrade(source: Source | undefined, plan?: SubscriptionPlanId): Promise<void> {
885+
async upgrade(source: Source | undefined, plan?: SubscriptionPlanId): Promise<boolean> {
886886
const scope = getLogScope();
887887

888-
if (!(await ensurePlusFeaturesEnabled())) return;
888+
if (!(await ensurePlusFeaturesEnabled())) return false;
889889

890890
let aborted = false;
891891
const promo = await this.container.productConfig.getApplicablePromo(this._subscription.state);
@@ -919,7 +919,7 @@ export class SubscriptionService implements Disposable {
919919
getSubscriptionPlanPriority(this._subscription.plan.effective.id) >=
920920
getSubscriptionPlanPriority(plan ?? SubscriptionPlanId.Pro)
921921
) {
922-
return;
922+
return true;
923923
}
924924
}
925925
} catch {}
@@ -982,7 +982,7 @@ export class SubscriptionService implements Disposable {
982982
aborted = !(await openUrl(this.container.urls.getGkDevUrl('purchase/checkout', query)));
983983

984984
if (aborted) {
985-
return;
985+
return false;
986986
}
987987

988988
telemetry?.dispose();
@@ -1016,6 +1016,8 @@ export class SubscriptionService implements Disposable {
10161016
if (refresh) {
10171017
void this.checkUpdatedSubscription(source);
10181018
}
1019+
1020+
return true;
10191021
}
10201022

10211023
@gate<SubscriptionService['validate']>(o => `${o?.force ?? false}`)

src/plus/gk/utils/-webview/acount.utils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import type { Uri } from 'vscode';
12
import { window } from 'vscode';
23
import type { Source } from '../../../../constants.telemetry';
34
import type { Container } from '../../../../container';
5+
import type { PlusFeatures } from '../../../../features';
46

57
export async function ensureAccount(container: Container, title: string, source: Source): Promise<boolean> {
68
while (true) {
@@ -52,3 +54,37 @@ export async function ensureAccount(container: Container, title: string, source:
5254

5355
return true;
5456
}
57+
58+
export async function ensureFeatureAccess(
59+
container: Container,
60+
title: string,
61+
feature: PlusFeatures,
62+
source: Source,
63+
repoPath?: string | Uri,
64+
): Promise<boolean> {
65+
if (!(await ensureAccount(container, title, source))) return false;
66+
67+
while (true) {
68+
const access = await container.git.access(feature, repoPath);
69+
if (access.allowed) break;
70+
71+
const upgrade = { title: 'Upgrade to Pro' };
72+
const cancel = { title: 'Cancel', isCloseAffordance: true };
73+
const result = await window.showWarningMessage(
74+
`${title}\n\nPlease upgrade to GitLens Pro to continue.`,
75+
{ modal: true },
76+
upgrade,
77+
cancel,
78+
);
79+
80+
if (result === upgrade) {
81+
if (await container.subscription.upgrade(source)) {
82+
continue;
83+
}
84+
}
85+
86+
return false;
87+
}
88+
89+
return true;
90+
}

0 commit comments

Comments
 (0)