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

Open
wants to merge 9 commits into
base: hitesh/make-autoedits-reactive
Choose a base branch
from
2 changes: 1 addition & 1 deletion lib/shared/src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ interface RawClientConfiguration {
experimentalSupercompletions: boolean
experimentalAutoeditsRendererTesting: boolean
experimentalAutoeditsConfigOverride: AutoEditsModelConfig | undefined
experimentalAutoeditsEnabled: boolean | undefined
experimentalAutoeditsEnabled: boolean
experimentalCommitMessage: boolean
experimentalNoodle: boolean
experimentalMinionAnthropicKey: string | undefined
Expand Down
115 changes: 115 additions & 0 deletions vscode/src/autoedits/autoedit-onboarding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {
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

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

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

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 feature which suggests advanced context-aware code edits as you navigate the codebase',
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
'Try Cody auto-edits: Experimental feature which suggests advanced context-aware code edits as you navigate the codebase',
'Try Cody Auto-Edits (experimental)? Cody will intelligently suggest next edits as you navigate the codebase.',

I don't love "as you navigate the codebase" though — does this trigger when you just open files, or when you're actively working on a file, or…?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I just realised this feature replaces the standard autocomplete behaviour too, which people probably have a very strong familiarity with. Is it easy to toggle this feature on/off via the Cody statusbar menu in the bottom right corner?

'Enable auto-edits'
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
'Enable auto-edits'
'Enable Auto-Edits'

)
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?


if (selection === 'Enable auto-edits') {
// Enable the setting programmatically
await vscode.workspace
.getConfiguration()
.update(
'cody.suggestions.mode',
'auto-edits (Experimental)',
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?

await vscode.commands.executeCommand(
'workbench.action.openSettings',
'cody.suggestions.mode'
)
}
}

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(): Promise<void> {
const info = await this.getAutoEditsOnboardingNotificationInfo()
await localStorage.setAutoEditsOnboardingNotificationInfo({
timesShown: info.timesShown + 1,
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()
}
}
78 changes: 71 additions & 7 deletions vscode/src/autoedits/create-autoedits-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,50 @@ import { type Observable, map } from 'observable-fns'
import * as vscode from 'vscode'

import {
type AuthenticatedAuthStatus,
type AuthStatus,
type ChatClient,
NEVER,
type PickResolvedConfiguration,
type UnauthenticatedAuthStatus,
type UserProductSubscription,
combineLatest,
createDisposables,
currentUserProductSubscription,
promiseFactoryToObservable,
skipPendingOperation,
} from '@sourcegraph/cody-shared'
import { isFreeUser } from '@sourcegraph/cody-shared/src/auth/types'
import { isRunningInsideAgent } from '../jsonrpc/isRunningInsideAgent'
import { AutoeditsProvider } from './autoedits-provider'
import { autoeditsOutputChannelLogger } from './output-channel-logger'

/**
* 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
}

export function createAutoEditsProvider({
config: { configuration },
authStatus,
chatClient,
autoeditsFeatureFlagEnabled,
}: AutoeditsItemProviderArgs): Observable<void> {
if (!configuration.experimentalAutoeditsEnabled) {
return NEVER
Expand All @@ -36,11 +58,24 @@ export function createAutoEditsProvider({
return NEVER
}

return promiseFactoryToObservable(async () => {
return await getAutoeditsProviderDocumentFilters()
}).pipe(
return combineLatest(
promiseFactoryToObservable(async () => await currentUserProductSubscription()),
promiseFactoryToObservable(async () => await getAutoeditsProviderDocumentFilters())
).pipe(
skipPendingOperation(),
createDisposables(documentFilters => {
createDisposables(([userProductSubscription, documentFilters]) => {
const userEligibilityInfo = isUserEligibleForAutoeditsFeature(
autoeditsFeatureFlagEnabled,
authStatus,
userProductSubscription
)
if (!userEligibilityInfo.isUserEligible) {
if (userEligibilityInfo.nonEligibilityReason) {
vscode.window.showInformationMessage(userEligibilityInfo.nonEligibilityReason)
}
return []
}

const provider = new AutoeditsProvider(chatClient)
return [
vscode.commands.registerCommand('cody.command.autoedits-manual-trigger', async () => {
Expand All @@ -58,3 +93,32 @@ export function createAutoEditsProvider({
export async function getAutoeditsProviderDocumentFilters(): Promise<vscode.DocumentFilter[]> {
return [{ scheme: 'file', language: '*' }, { notebookType: '*' }]
}

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: 'auto-edits is currently only supported in VS Code.',
}
}
// Free users are not eligible for auto-edits
if (isFreeUser(authStatus, productSubscription)) {
return {
isUserEligible: false,
nonEligibilityReason: 'auto-edits requires Cody Pro subscription.',
}
}

// Users with autoedits feature flag enabled are eligible for auto-edits
return {
isUserEligible: autoeditsFeatureFlagEnabled,
nonEligibilityReason: autoeditsFeatureFlagEnabled
? undefined
: 'auto-edits is an experimental feature and currently not enabled for your account. Please check back later.',
}
}
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
@@ -1,7 +1,6 @@
import * as vscode from 'vscode'

import {
type AuthStatus,
type ChatClient,
ClientConfigSingleton,
type ConfigurationInput,
Expand All @@ -25,7 +24,6 @@ import {
fromVSCodeEvent,
graphqlClient,
isDotCom,
isS2,
modelsService,
resolvedConfig,
setClientCapabilities,
Expand Down Expand Up @@ -714,14 +712,12 @@ function registerAutoEdits(chatClient: ChatClient, disposables: vscode.Disposabl
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,
})
}),
catchError(error => {
Expand All @@ -734,18 +730,6 @@ function registerAutoEdits(chatClient: ChatClient, disposables: vscode.Disposabl
)
}

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 @@ -766,29 +750,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
17 changes: 17 additions & 0 deletions vscode/src/services/LocalStorageProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
startWith,
} from '@sourcegraph/cody-shared'
import { type Observable, map } from 'observable-fns'
import type { AutoeditsNotificationInfo } from '../../src/autoedits/autoedit-onboarding'
import { isSourcegraphToken } from '../chat/protocol'
import type { GitHubDotComRepoMetaData } from '../repository/githubRepoMetadata'
import { EventEmitter } from '../testutils/mocks'
Expand All @@ -36,6 +37,9 @@ class LocalStorage implements LocalStorageForModelPreferences {
public readonly LAST_USED_ENDPOINT = 'SOURCEGRAPH_CODY_ENDPOINT'
private readonly MODEL_PREFERENCES_KEY = 'cody-model-preferences'
private readonly CODY_CHAT_MEMORY = 'cody-chat-memory'
private readonly AUTO_EDITS_ONBOARDING_NOTIFICATION_COUNT =
'cody-auto-edits-onboarding-notification-info'

public readonly keys = {
// LLM waitlist for the 09/12/2024 openAI o1 models
waitlist_o1: 'CODY_WAITLIST_LLM_09122024',
Expand Down Expand Up @@ -238,6 +242,19 @@ class LocalStorage implements LocalStorageForModelPreferences {
}
}

public async getAutoEditsOnboardingNotificationInfo(): Promise<AutoeditsNotificationInfo> {
return (
this.get<AutoeditsNotificationInfo>(this.AUTO_EDITS_ONBOARDING_NOTIFICATION_COUNT) ?? {
lastNotifiedTime: 0,
timesShown: 0,
}
)
}

public async setAutoEditsOnboardingNotificationInfo(info: AutoeditsNotificationInfo): Promise<void> {
await this.set(this.AUTO_EDITS_ONBOARDING_NOTIFICATION_COUNT, info)
}

public async setGitHubRepoAccessibility(data: GitHubDotComRepoMetaData[]): Promise<void> {
await this.set(this.GIT_REPO_ACCESSIBILITY_KEY, data)
}
Expand Down
Loading