Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contributions.json
Original file line number Diff line number Diff line change
Expand Up @@ -4906,7 +4906,7 @@
}
},
"gitlens.views.ai.generateChangelog": {
"label": "Generate Changelog",
"label": "Generate Changelog (Preview)",
"icon": "$(sparkle)",
"menus": {
"view/item/context": [
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7833,7 +7833,7 @@
},
{
"command": "gitlens.views.ai.generateChangelog",
"title": "Generate Changelog",
"title": "Generate Changelog (Preview)",
"icon": "$(sparkle)"
},
{
Expand Down
26 changes: 25 additions & 1 deletion src/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,31 @@ export type RepoFeatureAccess =
visibility?: RepositoryVisibility;
};

export type PlusFeatures = 'timeline' | 'worktrees' | 'graph' | 'launchpad' | 'startWork' | 'associateIssueWithBranch';
export type PlusFeatures = ProFeatures | AdvancedFeatures;

export type ProFeatures =
| 'timeline'
| 'worktrees'
| 'graph'
| 'launchpad'
| 'startWork'
| 'associateIssueWithBranch'
| ProAIFeatures;
export type ProAIFeatures = 'generateStashMessage' | 'explainCommit' | 'cloudPatchGenerateTitleAndDescription';

export type AdvancedFeatures = AdvancedAIFeatures;
export type AdvancedAIFeatures = 'generateChangelog';

export type AIFeatures = ProAIFeatures | AdvancedAIFeatures;

export function isAdvancedFeature(feature: PlusFeatures): feature is AdvancedFeatures {
switch (feature) {
case 'generateChangelog':
return true;
default:
return false;
}
}

export type FeaturePreviews = 'graph';
export const featurePreviews: FeaturePreviews[] = ['graph'];
Expand Down
10 changes: 9 additions & 1 deletion src/git/gitProviderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,15 @@ export class GitProviderService implements Disposable {
return { allowed: subscription.account?.verified !== false, subscription: { current: subscription } };
}

if (feature === 'launchpad' || feature === 'startWork' || feature === 'associateIssueWithBranch') {
if (
feature === 'launchpad' ||
feature === 'startWork' ||
feature === 'associateIssueWithBranch' ||
feature === 'generateStashMessage' ||
feature === 'explainCommit' ||
feature === 'cloudPatchGenerateTitleAndDescription' ||
feature === 'generateChangelog'
) {
return { allowed: false, subscription: { current: subscription, required: SubscriptionPlanId.Pro } };
}

Expand Down
51 changes: 51 additions & 0 deletions src/plus/ai/aiProviderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { primaryAIProviders } from '../../constants.ai';
import type { AIGenerateDraftEventData, Source, TelemetryEvents } from '../../constants.telemetry';
import type { Container } from '../../container';
import { CancellationError } from '../../errors';
import type { AIFeatures } from '../../features';
import { isAdvancedFeature } from '../../features';
import type { GitCommit } from '../../git/models/commit';
import { isCommit } from '../../git/models/commit';
import type { GitRevisionReference } from '../../git/models/reference';
Expand All @@ -13,6 +15,7 @@ import { uncommitted, uncommittedStaged } from '../../git/models/revision';
import { assertsCommitHasFullDetails } from '../../git/utils/commit.utils';
import { showAIModelPicker } from '../../quickpicks/aiModelPicker';
import { configuration } from '../../system/-webview/configuration';
import { getContext } from '../../system/-webview/context';
import type { Storage } from '../../system/-webview/storage';
import { supportedInVSCodeVersion } from '../../system/-webview/vscode';
import { debounce } from '../../system/function/debounce';
Expand All @@ -22,6 +25,7 @@ import { lazy } from '../../system/lazy';
import type { Deferred } from '../../system/promise';
import { getSettledValue } from '../../system/promise';
import type { ServerConnection } from '../gk/serverConnection';
import { ensureFeatureAccess } from '../gk/utils/-webview/acount.utils';
import type { AIActionType, AIModel, AIModelDescriptor } from './models/model';
import type { PromptTemplateContext } from './models/promptTemplates';
import type { AIProvider, AIRequestResult } from './models/provider';
Expand Down Expand Up @@ -284,11 +288,44 @@ export class AIProviderService implements Disposable {
return model;
}

private async ensureOrgAccess(): Promise<boolean> {
const orgEnabled = getContext('gitlens:gk:organization:ai:enabled');
if (orgEnabled === false) {
await window.showErrorMessage(`AI features have been disabled for your organization.`);
return false;
}

return true;
}

private async ensureFeatureAccess(feature: AIFeatures, source: Source): Promise<boolean> {
if (!(await this.ensureOrgAccess())) return false;

if (
!(await ensureFeatureAccess(
this.container,
isAdvancedFeature(feature)
? `Advanced AI features require a trial or GitLens Advanced.`
: `Pro AI features require a trial or GitLens Pro.`,
feature,
source,
))
) {
return false;
}

return true;
}

async explainCommit(
commitOrRevision: GitRevisionReference | GitCommit,
sourceContext: Source & { type: TelemetryEvents['ai/explain']['changeType'] },
options?: { cancellation?: CancellationToken; progress?: ProgressOptions },
): Promise<AISummarizeResult | undefined> {
if (!(await this.ensureFeatureAccess('explainCommit', sourceContext))) {
return undefined;
}

const diff = await this.container.git.diff(commitOrRevision.repoPath).getDiff?.(commitOrRevision.ref);
if (!diff?.contents) throw new Error('No changes found to explain.');

Expand Down Expand Up @@ -339,6 +376,8 @@ export class AIProviderService implements Disposable {
progress?: ProgressOptions;
},
): Promise<AISummarizeResult | undefined> {
if (!(await this.ensureOrgAccess())) return undefined;

const changes: string | undefined = await this.getChanges(changesOrRepo);
if (changes == null) return undefined;

Expand Down Expand Up @@ -377,6 +416,10 @@ export class AIProviderService implements Disposable {
codeSuggestion?: boolean;
},
): Promise<AISummarizeResult | undefined> {
if (!(await this.ensureFeatureAccess('cloudPatchGenerateTitleAndDescription', sourceContext))) {
return undefined;
}

const changes: string | undefined = await this.getChanges(changesOrRepo);
if (changes == null) return undefined;

Expand Down Expand Up @@ -423,6 +466,10 @@ export class AIProviderService implements Disposable {
progress?: ProgressOptions;
},
): Promise<AISummarizeResult | undefined> {
if (!(await this.ensureFeatureAccess('generateStashMessage', source))) {
return undefined;
}

const changes: string | undefined = await this.getChanges(changesOrRepo);
if (changes == null) {
options?.generating?.cancel();
Expand Down Expand Up @@ -458,6 +505,10 @@ export class AIProviderService implements Disposable {
source: Source,
options?: { cancellation?: CancellationToken; progress?: ProgressOptions },
): Promise<AIResult | undefined> {
if (!(await this.ensureFeatureAccess('generateChangelog', source))) {
return undefined;
}

const result = await this.sendRequest(
'generate-changelog',
async () => ({
Expand Down
10 changes: 6 additions & 4 deletions src/plus/gk/subscriptionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -882,10 +882,10 @@ export class SubscriptionService implements Disposable {
}

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

if (!(await ensurePlusFeaturesEnabled())) return;
if (!(await ensurePlusFeaturesEnabled())) return false;

let aborted = false;
const promo = await this.container.productConfig.getApplicablePromo(this._subscription.state);
Expand Down Expand Up @@ -919,7 +919,7 @@ export class SubscriptionService implements Disposable {
getSubscriptionPlanPriority(this._subscription.plan.effective.id) >=
getSubscriptionPlanPriority(plan ?? SubscriptionPlanId.Pro)
) {
return;
return true;
}
}
} catch {}
Expand Down Expand Up @@ -982,7 +982,7 @@ export class SubscriptionService implements Disposable {
aborted = !(await openUrl(this.container.urls.getGkDevUrl('purchase/checkout', query)));

if (aborted) {
return;
return false;
}

telemetry?.dispose();
Expand Down Expand Up @@ -1016,6 +1016,8 @@ export class SubscriptionService implements Disposable {
if (refresh) {
void this.checkUpdatedSubscription(source);
}

return true;
}

@gate<SubscriptionService['validate']>(o => `${o?.force ?? false}`)
Expand Down
36 changes: 36 additions & 0 deletions src/plus/gk/utils/-webview/acount.utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Uri } from 'vscode';
import { window } from 'vscode';
import type { Source } from '../../../../constants.telemetry';
import type { Container } from '../../../../container';
import type { PlusFeatures } from '../../../../features';

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

return true;
}

export async function ensureFeatureAccess(
container: Container,
title: string,
feature: PlusFeatures,
source: Source,
repoPath?: string | Uri,
): Promise<boolean> {
if (!(await ensureAccount(container, title, source))) return false;

while (true) {
const access = await container.git.access(feature, repoPath);
if (access.allowed) break;

const upgrade = { title: 'Upgrade to Pro' };
const cancel = { title: 'Cancel', isCloseAffordance: true };
const result = await window.showWarningMessage(
`${title}\n\nPlease upgrade to GitLens Pro to continue.`,
{ modal: true },
upgrade,
cancel,
);

if (result === upgrade) {
if (await container.subscription.upgrade(source)) {
continue;
}
}

return false;
}

return true;
}