Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auto-edits): adding autoedits onboarding setup for dotcom users #6463

Merged
merged 13 commits into from
Jan 9, 2025
2 changes: 1 addition & 1 deletion lib/shared/src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ interface RawClientConfiguration {
experimentalSupercompletions: boolean
experimentalAutoeditsRendererTesting: boolean
experimentalAutoeditsConfigOverride: AutoEditsModelConfig | undefined
experimentalAutoeditsEnabled: boolean | undefined
experimentalAutoeditsEnabled: boolean
experimentalCommitMessage: boolean
experimentalNoodle: boolean
experimentalMinionAnthropicKey: string | undefined
Expand Down
125 changes: 125 additions & 0 deletions vscode/src/autoedits/autoedit-onboarding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {
CodyAutoSuggestionMode,
FeatureFlag,
currentAuthStatus,
currentResolvedConfig,
currentUserProductSubscription,
featureFlagProvider,
storeLastValue,
} from '@sourcegraph/cody-shared'
import * as vscode from 'vscode'
import { localStorage } from '../services/LocalStorageProvider'
import { isUserEligibleForAutoeditsFeature } from './create-autoedits-provider'

export interface AutoeditsNotificationInfo {
lastNotifiedTime: number
timesShown: number
}

export class AutoeditsOnboarding implements vscode.Disposable {
private readonly MAX_AUTO_EDITS_ONBOARDING_NOTIFICATIONS = 3
private readonly MIN_TIME_DIFF_AUTO_EDITS_BETWEEN_NOTIFICATIONS_MS = 60 * 60 * 1000 // 1 hour
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TY!


private featureFlagAutoeditsExperimental = storeLastValue(
featureFlagProvider.evaluatedFeatureFlag(FeatureFlag.CodyAutoeditExperimentEnabledFeatureFlag)
)

public async showAutoeditsOnboardingIfEligible(): Promise<void> {
const shouldShowOnboardingPopup = await this.shouldShowAutoeditsOnboardingPopup()
if (shouldShowOnboardingPopup) {
await this.showAutoeditsOnboardingPopup()
}
}

private async showAutoeditsOnboardingPopup(): Promise<void> {
const selection = await vscode.window.showInformationMessage(
toolmantim marked this conversation as resolved.
Show resolved Hide resolved
'Try Cody Auto-Edits (experimental)? Cody will intelligently suggest next edits as you navigate the codebase.',
'Enable Auto-Edits',
"Don't Show Again"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a "Don't Show Again" option here? (like

public static readonly noThanksText = 'Don’t Show Again'
)? Otherwise people will get asked 3 times, yes, even if they're not interested?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added the text

await this.incrementAutoEditsOnboardingNotificationCount({ incrementCount: 1 })

if (selection === 'Enable Auto-Edits') {
// Enable the setting programmatically
await vscode.workspace
.getConfiguration()
.update(
'cody.suggestions.mode',
CodyAutoSuggestionMode.Autoedits,
vscode.ConfigurationTarget.Global
)

// Open VS Code settings UI and focus on the Cody Autoedits setting
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the thinking behind opening the settings panel?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to make them aware where to find the settings in case they want to turn on/off or switch between autocomplete/auto-edits.

await vscode.commands.executeCommand(
'workbench.action.openSettings',
'cody.suggestions.mode'
)
} else if (selection === "Don't Show Again") {
// If user doesn't want to see the notification again, increase number of shown notification by max limit + 1
// to prevent showing the notification again until the user restarts VS Code.
await this.incrementAutoEditsOnboardingNotificationCount({
incrementCount: this.MAX_AUTO_EDITS_ONBOARDING_NOTIFICATIONS + 1,
})
}
}

private async shouldShowAutoeditsOnboardingPopup(): Promise<boolean> {
const isAutoeditsEnabled = await this.isAutoeditsEnabled()
if (isAutoeditsEnabled) {
return false
}
const isUserEligible = await this.isUserEligibleForAutoeditsOnboarding()
if (!isUserEligible) {
return false
}
const isUnderNotificationLimit = await this.isAutoeditsNotificationsUnderLimit()
return isUnderNotificationLimit
}

private async incrementAutoEditsOnboardingNotificationCount(param: {
incrementCount: number
}): Promise<void> {
const info = await this.getAutoEditsOnboardingNotificationInfo()
await localStorage.setAutoEditsOnboardingNotificationInfo({
timesShown: info.timesShown + param.incrementCount,
lastNotifiedTime: Date.now(),
})
}

private async isAutoeditsEnabled(): Promise<boolean> {
const config = await currentResolvedConfig()
return config.configuration.experimentalAutoeditsEnabled
}

private async isAutoeditsNotificationsUnderLimit(): Promise<boolean> {
const info = await this.getAutoEditsOnboardingNotificationInfo()
return (
info.timesShown < this.MAX_AUTO_EDITS_ONBOARDING_NOTIFICATIONS &&
Date.now() - info.lastNotifiedTime > this.MIN_TIME_DIFF_AUTO_EDITS_BETWEEN_NOTIFICATIONS_MS
)
}

private async getAutoEditsOnboardingNotificationInfo(): Promise<AutoeditsNotificationInfo> {
return localStorage.getAutoEditsOnboardingNotificationInfo()
}

private async isUserEligibleForAutoeditsOnboarding(): Promise<boolean> {
const authStatus = currentAuthStatus()
const productSubscription = await currentUserProductSubscription()
const autoeditsFeatureFlag = this.isAutoeditsFeatureFlagEnabled()
const { isUserEligible } = isUserEligibleForAutoeditsFeature(
autoeditsFeatureFlag,
authStatus,
productSubscription
)
return isUserEligible
}

private isAutoeditsFeatureFlagEnabled(): boolean {
return !!this.featureFlagAutoeditsExperimental.value.last
}

dispose(): void {
this.featureFlagAutoeditsExperimental.subscription.unsubscribe()
}
}
105 changes: 98 additions & 7 deletions vscode/src/autoedits/create-autoedits-provider.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,62 @@
import { Observable, map } from 'observable-fns'
import { type Observable, map } from 'observable-fns'
import * as vscode from 'vscode'

import {
type AuthenticatedAuthStatus,
type AuthStatus,
type ChatClient,
CodyAutoSuggestionMode,
NEVER,
type PickResolvedConfiguration,
type UnauthenticatedAuthStatus,
type UserProductSubscription,
combineLatest,
createDisposables,
currentUserProductSubscription,
isFreeUser,
promiseFactoryToObservable,
skipPendingOperation,
} from '@sourcegraph/cody-shared'

import { isRunningInsideAgent } from '../jsonrpc/isRunningInsideAgent'
import type { FixupController } from '../non-stop/FixupController'

import { AutoeditsProvider } from './autoedits-provider'
import { autoeditsOutputChannelLogger } from './output-channel-logger'

const AUTOEDITS_NON_ELIGIBILITY_MESSAGES = {
ONLY_VSCODE_SUPPORT: 'Auto-Edits is currently only supported in VS Code.',
PRO_USER_ONLY: 'Auto-Edits requires Cody Pro subscription.',
FEATURE_FLAG_NOT_ELIGIBLE:
'Auto-Edits is an experimental feature and currently not enabled for your account. Please check back later.',
}

/**
* Information about a user's eligibility for auto-edits functionality.
*/
export interface AutoeditsUserEligibilityInfo {
/**
* Whether the user is eligible to use auto-edits.
*/
isUserEligible: boolean

/**
* The reason why the user is not eligible for auto-edits, if applicable.
* The message can be shown to the user, why auto-edits are not available to them.
*/
nonEligibilityReason?: string
}

interface AutoeditsItemProviderArgs {
config: PickResolvedConfiguration<{ configuration: true }>
authStatus: UnauthenticatedAuthStatus | Pick<AuthenticatedAuthStatus, 'authenticated' | 'endpoint'>
authStatus: AuthStatus
chatClient: ChatClient
autoeditsFeatureFlagEnabled: boolean
fixupController: FixupController
}

export function createAutoEditsProvider({
config: { configuration },
authStatus,
chatClient,
autoeditsFeatureFlagEnabled,
fixupController,
}: AutoeditsItemProviderArgs): Observable<void> {
if (!configuration.experimentalAutoeditsEnabled) {
Expand All @@ -40,9 +70,21 @@ export function createAutoEditsProvider({
return NEVER
}

return Observable.of(undefined).pipe(
return combineLatest(
promiseFactoryToObservable(async () => await currentUserProductSubscription())
).pipe(
skipPendingOperation(),
createDisposables(() => {
createDisposables(([userProductSubscription]) => {
const userEligibilityInfo = isUserEligibleForAutoeditsFeature(
autoeditsFeatureFlagEnabled,
authStatus,
userProductSubscription
)
if (!userEligibilityInfo.isUserEligible) {
handleAutoeditsNotificationForNonEligibleUser(userEligibilityInfo.nonEligibilityReason)
return []
}

const provider = new AutoeditsProvider(chatClient, fixupController)
return [
vscode.commands.registerCommand('cody.command.autoedits-manual-trigger', async () => {
Expand All @@ -59,3 +101,52 @@ export function createAutoEditsProvider({
map(() => undefined)
)
}

export async function handleAutoeditsNotificationForNonEligibleUser(
nonEligibilityReason?: string
): Promise<void> {
const switchToAutocompleteText = 'Switch to autocomplete'

const selection = await vscode.window.showErrorMessage(
`Error: ${nonEligibilityReason ?? AUTOEDITS_NON_ELIGIBILITY_MESSAGES.FEATURE_FLAG_NOT_ELIGIBLE}`,
switchToAutocompleteText
)
if (selection === switchToAutocompleteText) {
await vscode.workspace
.getConfiguration()
.update(
'cody.suggestions.mode',
CodyAutoSuggestionMode.Autocomplete,
vscode.ConfigurationTarget.Global
)
}
}

export function isUserEligibleForAutoeditsFeature(
autoeditsFeatureFlagEnabled: boolean,
authStatus: AuthStatus,
productSubscription: UserProductSubscription | null
): AutoeditsUserEligibilityInfo {
// Editors other than vscode are not eligible for auto-edits
if (isRunningInsideAgent()) {
return {
isUserEligible: false,
nonEligibilityReason: AUTOEDITS_NON_ELIGIBILITY_MESSAGES.ONLY_VSCODE_SUPPORT,
}
}
// Free users are not eligible for auto-edits
if (isFreeUser(authStatus, productSubscription)) {
return {
isUserEligible: false,
nonEligibilityReason: AUTOEDITS_NON_ELIGIBILITY_MESSAGES.PRO_USER_ONLY,
}
}

// Users with autoedits feature flag enabled are eligible for auto-edits
return {
isUserEligible: autoeditsFeatureFlagEnabled,
nonEligibilityReason: autoeditsFeatureFlagEnabled
? undefined
: AUTOEDITS_NON_ELIGIBILITY_MESSAGES.FEATURE_FLAG_NOT_ELIGIBLE,
}
}
7 changes: 7 additions & 0 deletions vscode/src/completions/inline-completion-item-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { autocompleteStageCounterLogger } from '../services/autocomplete-stage-c
import { recordExposedExperimentsToSpan } from '../services/open-telemetry/utils'
import { isInTutorial } from '../tutorial/helpers'

import { AutoeditsOnboarding } from '../autoedits/autoedit-onboarding'
import { ContextRankingStrategy } from '../completions/context/completions-context-ranker'
import type { CompletionBookkeepingEvent, CompletionItemID, CompletionLogID } from './analytics-logger'
import * as CompletionAnalyticsLogger from './analytics-logger'
Expand Down Expand Up @@ -138,6 +139,12 @@ export class InlineCompletionItemProvider
tracer = null,
...config
}: CodyCompletionItemProviderConfig) {
// Show the autoedits onboarding message if the user hasn't enabled autoedits
// but is eligible to use them as an alternative to autocomplete
const autoeditsOnboarding = new AutoeditsOnboarding()
autoeditsOnboarding.showAutoeditsOnboardingIfEligible()
this.disposables.push(autoeditsOnboarding)

// This is a static field to allow for easy access in the static `configuration` getter.
// There must only be one instance of this class at a time.
InlineCompletionItemProviderConfigSingleton.set({
Expand Down
41 changes: 5 additions & 36 deletions vscode/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { filter, map } from 'observable-fns'
import * as vscode from 'vscode'

import {
type AuthStatus,
type ChatClient,
ClientConfigSingleton,
type ConfigurationInput,
Expand All @@ -28,7 +27,6 @@ import {
fromVSCodeEvent,
graphqlClient,
isDotCom,
isS2,
modelsService,
resolvedConfig,
setClientCapabilities,
Expand Down Expand Up @@ -737,14 +735,12 @@ function registerAutoEdits(
isEqual(a[2], b[2])
)
}),
switchMap(([config, authStatus, autoeditEnabled]) => {
if (!shouldEnableExperimentalAutoedits(config, autoeditEnabled, authStatus)) {
return NEVER
}
switchMap(([config, authStatus, autoeditsFeatureFlagEnabled]) => {
return createAutoEditsProvider({
config,
authStatus,
chatClient,
autoeditsFeatureFlagEnabled,
fixupController,
})
}),
Expand All @@ -758,18 +754,6 @@ function registerAutoEdits(
)
}

function shouldEnableExperimentalAutoedits(
config: ResolvedConfiguration,
autoeditExperimentFlag: boolean,
authStatus: AuthStatus
): boolean {
// If the config is explicitly set in the vscode settings, use the setting instead of the feature flag.
if (config.configuration.experimentalAutoeditsEnabled !== undefined) {
return config.configuration.experimentalAutoeditsEnabled
}
return autoeditExperimentFlag && isS2(authStatus) && isRunningInsideAgent() === false
}

/**
* Registers autocomplete functionality.
*/
Expand All @@ -790,29 +774,14 @@ function registerAutocomplete(

disposables.push(
subscriptionDisposable(
combineLatest(
resolvedConfig,
authStatus,
featureFlagProvider.evaluatedFeatureFlag(
FeatureFlag.CodyAutoeditExperimentEnabledFeatureFlag
)
)
combineLatest(resolvedConfig, authStatus)
.pipe(
//TODO(@rnauta -> @sqs): It feels yuk to handle the invalidation outside of
//where the state is picked. It's also very tedious
distinctUntilChanged((a, b) => {
return (
isEqual(a[0].configuration, b[0].configuration) &&
isEqual(a[1], b[1]) &&
isEqual(a[2], b[2])
)
return isEqual(a[0].configuration, b[0].configuration) && isEqual(a[1], b[1])
}),
switchMap(([config, authStatus, autoeditEnabled]) => {
// If the auto-edit experiment is enabled, we don't need to load the completion provider
if (shouldEnableExperimentalAutoedits(config, autoeditEnabled, authStatus)) {
finishLoading()
return NEVER
}
switchMap(([config, authStatus]) => {
if (!authStatus.pendingValidation && !statusBarLoader) {
statusBarLoader = statusBar.addLoader({
title: 'Completion Provider is starting',
Expand Down
Loading
Loading