Skip to content

Commit

Permalink
Add support link for Cody Pro & Enterprise (#3330)
Browse files Browse the repository at this point in the history
Co-authored-by: Beatrix <beatrix@sourcegraph.com>
  • Loading branch information
toolmantim and abeatrix committed Mar 14, 2024
1 parent 9a4a5e4 commit d2e1186
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 10 deletions.
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

0 comments on commit d2e1186

Please sign in to comment.