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

Add support link for Cody Pro & Enterprise #3330

Merged
merged 11 commits into from Mar 14, 2024
2 changes: 2 additions & 0 deletions vscode/CHANGELOG.md
Expand Up @@ -6,6 +6,8 @@ This is a log of all notable changes to Cody for VS Code. [Unreleased] changes a

### Added

- Added support links for Cody Pro and Enterprise users. [pull/3330](https://github.com/sourcegraph/cody/pull/3330)

### Fixed

### Changed
Expand Down
1 change: 1 addition & 0 deletions vscode/src/chat/protocol.ts
Expand Up @@ -184,6 +184,7 @@ export const CODY_DOC_URL = new URL('https://sourcegraph.com/docs/cody')
// Community and support
export const DISCORD_URL = new URL('https://discord.gg/s2qDtYGnAE')
export const CODY_FEEDBACK_URL = new URL('https://github.com/sourcegraph/cody/issues/new/choose')
export const CODY_SUPPORT_URL = new URL('https://help.sourcegraph.com/hc/en-us/requests/new')
// Account
export const ACCOUNT_UPGRADE_URL = new URL('https://sourcegraph.com/cody/subscription')
export const ACCOUNT_USAGE_URL = new URL('https://sourcegraph.com/cody/manage')
Expand Down
7 changes: 5 additions & 2 deletions vscode/src/main.ts
Expand Up @@ -276,6 +276,8 @@ const register = async (
)
}

const statusBar = createStatusBar()

// Adds a change listener to the auth provider that syncs the auth status
authProvider.addChangeListener(async (authStatus: AuthStatus) => {
// Chat Manager uses Simple Context Provider
Expand All @@ -299,10 +301,13 @@ const register = async (

parallelPromises.push(setupAutocomplete())
await Promise.all(parallelPromises)
statusBar.syncAuthStatus(authStatus)
})

// Sync initial auth status
await chatManager.syncAuthStatus(authProvider.getAuthStatus())
ModelProvider.onConfigChange(initialConfig.experimentalOllamaChat)
statusBar.syncAuthStatus(authProvider.getAuthStatus())

const commandsManager = platform.createCommandsProvider?.()
setCommandController(commandsManager)
Expand Down Expand Up @@ -349,8 +354,6 @@ const register = async (
vscode.commands.registerCommand('cody.command.explain-output', a => executeExplainOutput(a))
)

const statusBar = createStatusBar()

disposables.push(
// Tests
// Access token - this is only used in configuration tests
Expand Down
18 changes: 14 additions & 4 deletions vscode/src/services/FeedbackOptions.ts
@@ -1,20 +1,30 @@
import * as vscode from 'vscode'

import { CODY_DOC_URL, CODY_FEEDBACK_URL, DISCORD_URL } from '../chat/protocol'
import { CODY_DOC_URL, CODY_FEEDBACK_URL, CODY_SUPPORT_URL, DISCORD_URL } from '../chat/protocol'

export const FeedbackOptionItems = [
// Support items for paid users (e.g Enterprise Users and Cody Pro Users)
export const PremiumSupportItems = [
{
label: '$(feedback) Cody Feedback',
label: '$(question) Cody Support',
async onSelect(): Promise<void> {
await vscode.env.openExternal(vscode.Uri.parse(CODY_FEEDBACK_URL.href))
await vscode.env.openExternal(vscode.Uri.parse(CODY_SUPPORT_URL.href))
},
},
]

export const FeedbackOptionItems = [
{
label: '$(remote-explorer-documentation) Cody Documentation',
async onSelect(): Promise<void> {
await vscode.env.openExternal(vscode.Uri.parse(CODY_DOC_URL.href))
},
},
{
label: '$(feedback) Cody Feedback',
async onSelect(): Promise<void> {
await vscode.env.openExternal(vscode.Uri.parse(CODY_FEEDBACK_URL.href))
},
},
{
label: '$(organization) Cody Discord Channel',
async onSelect(): Promise<void> {
Expand Down
5 changes: 5 additions & 0 deletions vscode/src/services/SidebarCommands.ts
Expand Up @@ -6,6 +6,7 @@ import {
ACCOUNT_USAGE_URL,
CODY_DOC_URL,
CODY_FEEDBACK_URL,
CODY_SUPPORT_URL,
DISCORD_URL,
} from '../chat/protocol'
import { releaseNotesURL } from '../release'
Expand Down Expand Up @@ -60,6 +61,10 @@ export function registerSidebarCommands(): vscode.Disposable[] {
logSidebarClick('documentation')
void vscode.commands.executeCommand('vscode.open', CODY_DOC_URL.href)
}),
vscode.commands.registerCommand('cody.sidebar.support', () => {
logSidebarClick('support')
void vscode.commands.executeCommand('vscode.open', CODY_SUPPORT_URL.href)
}),
vscode.commands.registerCommand('cody.sidebar.feedback', () => {
logSidebarClick('feedback')
void vscode.commands.executeCommand('vscode.open', CODY_FEEDBACK_URL.href)
Expand Down
18 changes: 15 additions & 3 deletions vscode/src/services/StatusBar.ts
@@ -1,11 +1,11 @@
import * as vscode from 'vscode'

import { type Configuration, isCodyIgnoredFile } from '@sourcegraph/cody-shared'
import { type AuthStatus, type Configuration, isCodyIgnoredFile } from '@sourcegraph/cody-shared'

import { getConfiguration } from '../configuration'

import { getGhostHintEnablement } from '../commands/GhostHintDecorator'
import { FeedbackOptionItems } from './FeedbackOptions'
import { FeedbackOptionItems, PremiumSupportItems } from './FeedbackOptions'
import { enableDebugMode } from './utils/export-logs'

interface StatusBarError {
Expand All @@ -27,6 +27,7 @@ export interface CodyStatusBar {
): () => void
addError(error: StatusBarError): () => void
hasError(error: StatusBarErrorName): boolean
syncAuthStatus(newStatus: AuthStatus): void
}

const DEFAULT_TEXT = '$(cody-logo-heavy)'
Expand All @@ -50,6 +51,7 @@ export function createStatusBar(): CodyStatusBar {
statusBarItem.command = 'cody.status-bar.interacted'
statusBarItem.show()

let authStatus: AuthStatus | undefined
const command = vscode.commands.registerCommand(statusBarItem.command, async () => {
const workspaceConfig = vscode.workspace.getConfiguration()
const config = getConfiguration(workspaceConfig)
Expand Down Expand Up @@ -86,6 +88,13 @@ export function createStatusBar(): CodyStatusBar {
}
}

function createFeedbackAndSupportItems(): StatusBarItem[] {
const isPaidUser = authStatus?.isLoggedIn && !authStatus?.userCanUpgrade
const paidSupportItems = isPaidUser ? PremiumSupportItems : []
// Display to paid users (e.g. Enterprise users or Cody Pro uers) only
return [...paidSupportItems, ...FeedbackOptionItems]
}

if (errors.length > 0) {
errors.map(error => error.error.onShow?.())
}
Expand Down Expand Up @@ -179,7 +188,7 @@ export function createStatusBar(): CodyStatusBar {
},
},
{ label: 'feedback & support', kind: vscode.QuickPickItemKind.Separator },
...FeedbackOptionItems,
...createFeedbackAndSupportItems(),
]
quickPick.title = 'Cody Settings'
quickPick.placeholder = 'Choose an option'
Expand Down Expand Up @@ -308,6 +317,9 @@ export function createStatusBar(): CodyStatusBar {
hasError(errorName: StatusBarErrorName): boolean {
return errors.some(e => e.error.errorType === errorName)
},
syncAuthStatus(newStatus: AuthStatus) {
authStatus = newStatus
},
dispose() {
statusBarItem.dispose()
command.dispose()
Expand Down
4 changes: 4 additions & 0 deletions vscode/src/services/tree-views/TreeViewProvider.ts
Expand Up @@ -80,6 +80,10 @@ export class TreeViewProvider implements vscode.TreeDataProvider<vscode.TreeItem
continue
}

if (item.requirePaid && this.authStatus?.userCanUpgrade === true) {
continue
}

const treeItem = new vscode.TreeItem({ label: item.title })
treeItem.id = item.id
treeItem.iconPath = new vscode.ThemeIcon(item.icon)
Expand Down
7 changes: 7 additions & 0 deletions vscode/src/services/tree-views/treeViewItems.ts
Expand Up @@ -18,6 +18,7 @@ export interface CodySidebarTreeItem {
requireFeature?: FeatureFlag
requireUpgradeAvailable?: boolean
requireDotCom?: boolean
requirePaid?: boolean
}

/**
Expand Down Expand Up @@ -68,6 +69,12 @@ const supportItems: CodySidebarTreeItem[] = [
icon: 'book',
command: { command: 'cody.sidebar.documentation' },
},
{
title: 'Support',
icon: 'question',
command: { command: 'cody.sidebar.support' },
requirePaid: true,
},
{
title: 'Feedback',
icon: 'feedback',
Expand Down
74 changes: 74 additions & 0 deletions vscode/test/e2e/support-menu-items.test.ts
@@ -0,0 +1,74 @@
import { expect } from '@playwright/test'
import * as mockServer from '../fixtures/mock-server'

import { sidebarSignin } from './common'
import { type DotcomUrlOverride, type ExpectedEvents, test as baseTest } from './helpers'

const test = baseTest.extend<DotcomUrlOverride>({ dotcomUrl: mockServer.SERVER_URL })

test.extend<ExpectedEvents>({
// list of events we expect this test to log, add to this list as needed
expectedEvents: [
'CodyInstalled',
'CodyVSCodeExtension:Auth:failed',
'CodyVSCodeExtension:auth:clickOtherSignInOptions',
'CodyVSCodeExtension:login:clicked',
'CodyVSCodeExtension:auth:selectSigninMenu',
'CodyVSCodeExtension:auth:fromToken',
'CodyVSCodeExtension:Auth:connected',
],
})('shows no support link for free users', async ({ page, sidebar }) => {
await sidebarSignin(page, sidebar)

const supportLocator = page.getByRole('treeitem', { name: 'Support' }).locator('a')
expect(supportLocator).not.toBeVisible()

// Check it's not in treeview

const supportButton = page.getByLabel('Cody Support, feedback & support').locator('div')
expect(supportButton).not.toBeVisible()

// Check it's not in settings quickpick

const statusBarButton = page.getByRole('button', { name: 'cody-logo-heavy, Cody Settings' })
await statusBarButton.click()

const input = page.getByPlaceholder('Choose an option')
await input.fill('support')

const supportItem = page.getByLabel('question Cody Support')
expect(supportItem).not.toBeVisible()
})

test.extend<ExpectedEvents>({
// list of events we expect this test to log, add to this list as needed
expectedEvents: [
'CodyInstalled',
'CodyVSCodeExtension:Auth:failed',
'CodyVSCodeExtension:auth:clickOtherSignInOptions',
'CodyVSCodeExtension:login:clicked',
'CodyVSCodeExtension:auth:selectSigninMenu',
'CodyVSCodeExtension:auth:fromToken',
'CodyVSCodeExtension:Auth:connected',
],
})('shows support link for pro users', async ({ page, sidebar }) => {
await fetch(`${mockServer.SERVER_URL}/.test/currentUser/codyProEnabled`, { method: 'POST' })

await sidebarSignin(page, sidebar)

// Check it's in treeview

const supportLocator = page.getByRole('treeitem', { name: 'Support' }).locator('a')
expect(supportLocator).toBeVisible()

// Check it's in settings quickpick

const statusBarButton = page.getByRole('button', { name: 'cody-logo-heavy, Cody Settings' })
await statusBarButton.click()

const input = page.getByPlaceholder('Choose an option')
await input.fill('support')

const supportItem = page.getByLabel('question Cody Support')
expect(supportItem).toBeVisible()
})
24 changes: 23 additions & 1 deletion vscode/test/fixtures/mock-server.ts
Expand Up @@ -294,6 +294,7 @@ export class MockServer {
})

let attribution = false
let codyPro = false
app.post('/.api/graphql', (req, res) => {
if (req.headers.authorization !== `token ${VALID_TOKEN}`) {
res.sendStatus(401)
Expand Down Expand Up @@ -328,12 +329,29 @@ export class MockServer {
})
)
break
case 'CurrentUserCodySubscription':
res.send(
JSON.stringify({
data: {
currentUser: {
codySubscription: {
status: 'ACTIVE',
plan: codyPro ? 'PRO' : 'FREE',
applyProRateLimits: codyPro,
currentPeriodStartAt: '2021-01-01T00:00:00Z',
currentPeriodEndAt: '2022-01-01T00:00:00Z',
},
},
},
})
)
break
case 'CurrentUserCodyProEnabled':
res.send(
JSON.stringify({
data: {
currentUser: {
codyProEnabled: false,
codyProEnabled: codyPro,
},
},
})
Expand Down Expand Up @@ -421,6 +439,10 @@ export class MockServer {
}
})

app.post('/.test/currentUser/codyProEnabled', (req, res) => {
codyPro = true
res.sendStatus(200)
})
app.post('/.test/attribution/enable', (req, res) => {
attribution = true
res.sendStatus(200)
Expand Down