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 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 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 agent/src/AgentWorkspaceConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('AgentWorkspaceConfiguration', () => {
expect(config.get('cody.telemetry.level')).toBe('agent')
// clientName undefined because custom JSON specified telemetry with level alone.
expect(config.get('cody.telemetry.clientName')).toBe('test-client')
expect(config.get('cody.autocomplete.enabled')).toBe(true)
expect(config.get('cody.suggestions.mode')).toBe('autocomplete')
expect(config.get('cody.autocomplete.advanced.provider')).toBe('anthropic')
expect(config.get('cody.autocomplete.advanced.model')).toBe('claude-2')
expect(config.get('cody.advanced.agent.running')).toBe(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class ConfigUtils {
"""
{
"cody.debug": true,
"cody.autocomplete.enabled": true
"cody.suggestions.mode": "autocomplete"
}
"""
val result = ConfigUtil.getCustomConfiguration(mockProject, input)
Expand All @@ -39,7 +39,7 @@ class ConfigUtils {
"""
{
"cody.debug": true,
"cody.autocomplete.enabled": true,
"cody.suggestions.mode": "autocomplete"
}
"""
val result = ConfigUtil.getCustomConfiguration(mockProject, input)
Expand All @@ -55,7 +55,7 @@ class ConfigUtils {
{
// This is a comment
"cody.debug": true,
"cody.autocomplete.enabled": true,
"cody.suggestions.mode": "autocomplete"
}
"""
val result = ConfigUtil.getCustomConfiguration(mockProject, input)
Expand Down
20 changes: 19 additions & 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 Expand Up @@ -188,6 +188,24 @@ export enum CodyIDE {
StandaloneWeb = 'StandaloneWeb',
}

/**
* These values must match the enum values in cody.suggestions.mode in vscode/package.json
*/
export enum CodyAutoSuggestionMode {
/**
* The suggestion mode where suggestions come from the OpenAI completions API. This is the default mode.
*/
Autocomplete = 'autocomplete',
/**
* The suggestion mode where suggestions come from the Cody AI agent chat API.
*/
Autoedits = 'auto-edits (Experimental)',
/**
* Disable Cody suggestions altogether.
*/
Off = 'off',
}

export type AutocompleteProviderID = keyof typeof AUTOCOMPLETE_PROVIDER_ID

export const AUTOCOMPLETE_PROVIDER_ID = {
Expand Down
25 changes: 15 additions & 10 deletions vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -409,13 +409,13 @@
"command": "cody.autocomplete.openTraceView",
"category": "Cody",
"title": "Open Autocomplete Trace View",
"enablement": "cody.activated && config.cody.autocomplete.enabled"
"enablement": "cody.activated && config.cody.suggestions.mode === 'autocomplete'"
},
{
"command": "cody.autocomplete.manual-trigger",
"category": "Cody",
"title": "Trigger Autocomplete at Cursor",
"enablement": "cody.activated && config.cody.autocomplete.enabled && !editorReadonly && !editorHasSelection && !inlineSuggestionsVisible"
"enablement": "cody.activated && config.cody.suggestions.mode === 'autocomplete' && !editorReadonly && !editorHasSelection && !inlineSuggestionsVisible"
},
{
"command": "cody.chat.signIn",
Expand Down Expand Up @@ -578,7 +578,7 @@
{
"command": "cody.command.autoedits-manual-trigger",
"title": "Autoedits Manual Trigger",
"enablement": "cody.activated && config.cody.experimental.autoedits.enabled"
"enablement": "cody.activated && config.cody.suggestions.mode == 'auto-edits (Experimental)'"
}
],
"keybindings": [
Expand Down Expand Up @@ -687,7 +687,7 @@
{
"command": "cody.autocomplete.manual-trigger",
"key": "alt+\\",
"when": "editorTextFocus && !editorHasSelection && config.cody.autocomplete.enabled && !inlineSuggestionsVisible"
"when": "editorTextFocus && !editorHasSelection && config.cody.suggestions.mode === 'autocomplete' && !inlineSuggestionsVisible"
},
{
"command": "cody.fixup.acceptNearest",
Expand Down Expand Up @@ -739,7 +739,7 @@
{
"command": "cody.supersuggest.testExample",
"key": "ctrl+alt+enter",
"when": "cody.activated && config.cody.experimental.autoedits.enabled"
"when": "cody.activated && config.cody.suggestions.mode == 'auto-edits (Experimental)'"
}
],
"submenus": [
Expand Down Expand Up @@ -1001,11 +1001,16 @@
}
]
},
"cody.autocomplete.enabled": {
"order": 5,
"type": "boolean",
"markdownDescription": "Enables code autocompletions.",
"default": true
"cody.suggestions.mode": {
"type": "string",
"enum": ["autocomplete", "auto-edits (Experimental)", "off"],
"enumDescriptions": [
"Suggests standard code completions as you type.",
"Suggests advanced context-aware code edits as you navigate the codebase. Experimental feature for Pro and Enterprise users.",
"Disables all suggestions."
],
"default": "autocomplete",
"markdownDescription": "Controls the suggestion mode for Cody"
},
"cody.autocomplete.triggerDelay": {
"order": 5,
Expand Down
96 changes: 96 additions & 0 deletions vscode/src/autoedits/autoedit-onboarding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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 class AutoeditsOnboarding implements vscode.Disposable {
private readonly MAX_AUTO_EDITS_ONBOARDING_NOTIFICATIONS = 3

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"as you navigate the codebase" seems to reflect that user doesn't have to type anything to get suggestions, they can get suggestion even if they move the cursor in the file.
Yes, they can get suggestion even if you just open the file. One example is - if user have made modification in some other files, model might suggest the change in the current file based on where your cursor is currently placed

'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 Author

Choose a reason for hiding this comment

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

done

)
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


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?

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'
)
}
}

private async shouldShowAutoeditsOnboardingPopup(): Promise<boolean> {
const isUserEligible = await this.isUserEligibleForAutoeditsOnboarding()
const isAutoeditsDisabled = await this.isAutoeditsDisabled()
const isUnderNotificationLimit = await this.isAutoeditsNotificationsUnderLimit()
return isUserEligible && isAutoeditsDisabled && isUnderNotificationLimit
}

private async incrementAutoEditsOnboardingNotificationCount(): Promise<void> {
const count = await this.getAutoEditsOnboardingNotificationCount()
await localStorage.setAutoEditsOnboardingNotificationCount(count + 1)
}

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

private async isAutoeditsNotificationsUnderLimit(): Promise<boolean> {
const count = await this.getAutoEditsOnboardingNotificationCount()
return count < this.MAX_AUTO_EDITS_ONBOARDING_NOTIFICATIONS
Copy link
Member

Choose a reason for hiding this comment

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

If we decide to show it multiple times, we should add a progressive timeout between prompts to avoid coming across as spammy.

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 min 1 hour timeout between 2 calls

}

private async getAutoEditsOnboardingNotificationCount(): Promise<number> {
return await localStorage.getAutoEditsOnboardingNotificationCount()
}
Copy link
Member

Choose a reason for hiding this comment

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

nit: no need to await in cases where we only need to return a promise

Suggested change
private async getAutoEditsOnboardingNotificationCount(): Promise<number> {
return await localStorage.getAutoEditsOnboardingNotificationCount()
}
private getAutoEditsOnboardingNotificationCount(): Promise<number> {
return localStorage.getAutoEditsOnboardingNotificationCount()
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done


private async isUserEligibleForAutoeditsOnboarding(): Promise<boolean> {
const authStatus = currentAuthStatus()
const productSubsubscription = await currentUserProductSubscription()
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const productSubsubscription = await currentUserProductSubscription()
const productSubscription = await currentUserProductSubscription()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

const autoeditsFeatureFlag = this.isAutoeditsFeatureFlagEnabled()
return isUserEligibleForAutoeditsFeature(
autoeditsFeatureFlag,
authStatus,
productSubsubscription
)
}

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

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

import {
type AuthStatus,
type ChatClient,
NEVER,
type PickResolvedConfiguration,
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'

interface AutoeditsItemProviderArgs {
config: PickResolvedConfiguration<{ configuration: true }>
authStatus: AuthStatus
chatClient: ChatClient
autoeditsFeatureFlagEnabled: boolean
}

export function createAutoEditsProvider({
config: { configuration },
authStatus,
chatClient,
autoeditsFeatureFlagEnabled,
}: AutoeditsItemProviderArgs): Observable<void> {
if (!configuration.experimentalAutoeditsEnabled) {
return NEVER
}

if (!authStatus.authenticated) {
if (!authStatus.pendingValidation) {
autoeditsOutputChannelLogger.logDebug('createProvider', 'You are not signed in.')
}
return NEVER
}

return combineLatest(
promiseFactoryToObservable(async () => await currentUserProductSubscription()),
promiseFactoryToObservable(async () => await getAutoeditsProviderDocumentFilters())
).pipe(
skipPendingOperation(),
createDisposables(([userProductSubscription, documentFilters]) => {
if (
!isUserEligibleForAutoeditsFeature(
autoeditsFeatureFlagEnabled,
authStatus,
userProductSubscription
)
Copy link
Member

Choose a reason for hiding this comment

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

Can we move this check one level higher to the registerAutoEdits function in main.ts. If a user is not eligible, we should not attempt to call the createAutoEditsProvider function.

Copy link
Contributor Author

@hitesh-1997 hitesh-1997 Jan 7, 2025

Choose a reason for hiding this comment

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

any specific reason why main.ts could be better here ?
I liked it here since that puts all the logic for creating/decide if not to create provider in a single place. Also, if the logic becomes more complex (such as the proposal to show pop-up to the non eligible users ) we want to add condition under the if, so we can do this in the same file.

Do you think it would be okay to just keep it here ?

Copy link
Member

Choose a reason for hiding this comment

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

I assumed we still have a check for the feature flag at the top level, but now I see that we moved all conditions into one place. That was the primary purpose of this suggestion 👍

) {
throw new Error(
'User is not eligible for auto-edits. Can not initialize auto-edits provider.'
Copy link
Member

Choose a reason for hiding this comment

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

It would be helpful for debugging if we named the reason here or at least logged it to the output channel. That way, when users come to us with questions about this error, we can triage it faster.

Copy link
Contributor Author

@hitesh-1997 hitesh-1997 Jan 7, 2025

Choose a reason for hiding this comment

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

Added the message which we will show to the user as per the discussion. A quick snapshot below, when free user tried to access auto-edits:
image

)
}

const provider = new AutoeditsProvider(chatClient)
return [
vscode.commands.registerCommand('cody.command.autoedits-manual-trigger', async () => {
await vscode.commands.executeCommand('editor.action.inlineSuggest.hide')
await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger')
}),
vscode.languages.registerInlineCompletionItemProvider(documentFilters, provider),
provider,
]
}),
map(() => undefined)
)
}

export async function getAutoeditsProviderDocumentFilters(): Promise<vscode.DocumentFilter[]> {
return [{ scheme: 'file', language: '*' }, { notebookType: '*' }]
}

export function isUserEligibleForAutoeditsFeature(
autoeditsFeatureFlagEnabled: boolean,
authStatus: AuthStatus,
productSubsubscription: UserProductSubscription | null
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
productSubsubscription: UserProductSubscription | null
productSubscription: UserProductSubscription | null

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

): boolean {
// Editors other than vscode are not eligible for auto-edits
if (isRunningInsideAgent()) {
return false
}
// Free users are not eligible for auto-edits
if (isFreeUser(authStatus, productSubsubscription)) {
return false
}
// Users with autoedits feature flag enabled are eligible for auto-edits
return autoeditsFeatureFlagEnabled
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function createInlineCompletionItemProvider({
) {
throw new Error(
'The setting `config.autocomplete` evaluated to `false`. It must be true when running inside the agent. ' +
'To fix this problem, make sure that the setting cody.autocomplete.enabled has the value true.'
'To fix this problem, make sure that the setting cody.suggestions.mode has the value autocomplete.'
)
}
return NEVER
Expand Down
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
Loading
Loading