mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 18:51:59 +00:00
cody: add support for intel mac and linux (#54405)
This commit is contained in:
parent
933c504fa1
commit
d19f0e79fb
@ -1,6 +1,6 @@
|
||||
.container {
|
||||
display: inline-block;
|
||||
padding: 0.4rem 0.4rem 0.3rem 0.7rem;
|
||||
padding: 0.4rem;
|
||||
font-size: 0.9rem;
|
||||
min-width: calc(100% - 1.1rem);
|
||||
}
|
||||
@ -9,7 +9,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
gap: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.open-close-button {
|
||||
|
||||
@ -66,7 +66,7 @@ export type Config = Pick<
|
||||
export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disposable, IdleRecipeRunner {
|
||||
private isMessageInProgress = false
|
||||
private cancelCompletionCallback: (() => void) | null = null
|
||||
private webview?: Omit<vscode.Webview, 'postMessage'> & {
|
||||
public webview?: Omit<vscode.Webview, 'postMessage'> & {
|
||||
postMessage(message: ExtensionMessage): Thenable<boolean>
|
||||
}
|
||||
|
||||
@ -116,11 +116,8 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
|
||||
vscode.workspace.onDidChangeWorkspaceFolders(async () => {
|
||||
await this.updateCodebaseContext()
|
||||
}),
|
||||
vscode.commands.registerCommand('cody.app.sync', () => this.syncLocalAppState()),
|
||||
vscode.commands.registerCommand('cody.auth.sync', () => this.syncAuthStatus())
|
||||
)
|
||||
this.authProvider.init().catch(error => console.log(error))
|
||||
|
||||
const codyConfig = vscode.workspace.getConfiguration('cody')
|
||||
const tokenLimit = codyConfig.get<number>('provider.limit.prompt')
|
||||
const solutionLimit = codyConfig.get<number>('provider.limit.solution') || SOLUTION_TOKEN_LENGTH
|
||||
@ -176,6 +173,10 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
|
||||
public onConfigurationChange(newConfig: Config): void {
|
||||
debug('ChatViewProvider:onConfigurationChange', '')
|
||||
this.config = newConfig
|
||||
const authStatus = this.authProvider.getAuthStatus()
|
||||
if (authStatus.endpoint) {
|
||||
this.config.serverEndpoint = authStatus.endpoint
|
||||
}
|
||||
this.configurationChangeEvent.fire()
|
||||
}
|
||||
|
||||
@ -651,14 +652,6 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display app state in webview view that is used during Signin flow
|
||||
*/
|
||||
public async syncLocalAppState(): Promise<void> {
|
||||
// Notify webview that app is installed
|
||||
await this.webview?.postMessage({ type: 'app-state', isInstalled: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete history from current chat history and local storage
|
||||
*/
|
||||
@ -763,7 +756,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
|
||||
// update codebase context on configuration change
|
||||
await this.updateCodebaseContext()
|
||||
await this.webview?.postMessage({ type: 'config', config: configForWebview, authStatus })
|
||||
debug('Cody:publishConfig', 'webview', { verbose: authStatus })
|
||||
debug('Cody:publishConfig', 'configForWebview', { verbose: configForWebview })
|
||||
}
|
||||
|
||||
this.disposables.push(this.configurationChangeEvent.event(() => send()))
|
||||
@ -830,6 +823,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
|
||||
_token: vscode.CancellationToken
|
||||
): Promise<void> {
|
||||
this.webview = webviewView.webview
|
||||
this.authProvider.webview = webviewView.webview
|
||||
|
||||
const extensionPath = vscode.Uri.file(this.extensionPath)
|
||||
const webviewPath = vscode.Uri.joinPath(extensionPath, 'dist')
|
||||
|
||||
@ -61,13 +61,6 @@ export const CODY_FEEDBACK_URL = new URL(
|
||||
export const LOCAL_APP_URL = new URL('http://localhost:3080')
|
||||
export const APP_LANDING_URL = new URL('https://about.sourcegraph.com/app')
|
||||
export const APP_CALLBACK_URL = new URL('sourcegraph://user/settings/tokens/new/callback')
|
||||
// TODO: Update URLs to always point to the latest app release: https://github.com/sourcegraph/sourcegraph/issues/53511
|
||||
export const APP_DOWNLOAD_URLS: { [os: string]: { [arch: string]: string } } = {
|
||||
darwin: {
|
||||
arm64: 'https://github.com/sourcegraph/sourcegraph/releases/download/app-v2023.6.16%2B1314.6c2d49d47c/Cody_2023.6.16+1314.6c2d49d47c_aarch64.dmg',
|
||||
x64: 'https://github.com/sourcegraph/sourcegraph/releases/download/app-v2023.6.16%2B1314.6c2d49d47c/Cody_2023.6.16+1314.6c2d49d47c_x64.dmg',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* The status of a users authentication, whether they're authenticated and have a
|
||||
@ -147,3 +140,22 @@ export function isDotCom(url: string): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// The OS and Arch support for Cody app
|
||||
export function isOsSupportedByApp(os?: string, arch?: string): boolean {
|
||||
if (!os || !arch) {
|
||||
return false
|
||||
}
|
||||
return os === 'darwin' || os === 'linux'
|
||||
}
|
||||
|
||||
// Map the Arch to the app's supported Arch
|
||||
export function archConvertor(arch: string): string {
|
||||
switch (arch) {
|
||||
case 'arm64':
|
||||
return 'aarch64'
|
||||
case 'x64':
|
||||
return 'x86_64'
|
||||
}
|
||||
return arch
|
||||
}
|
||||
|
||||
@ -115,6 +115,7 @@ const register = async (
|
||||
} = await configureExternalServices(initialConfig, rgPath, editor)
|
||||
|
||||
const authProvider = new AuthProvider(initialConfig, secretStorage, localStorage)
|
||||
await authProvider.init()
|
||||
|
||||
// Create chat webview
|
||||
const chatProvider = new ChatViewProvider(
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
isLocalApp,
|
||||
isLoggedIn as isAuthed,
|
||||
unauthenticatedStatus,
|
||||
ExtensionMessage,
|
||||
} from '../chat/protocol'
|
||||
import { newAuthStatus } from '../chat/utils'
|
||||
import { logEvent } from '../event-logger'
|
||||
@ -30,6 +31,9 @@ export class AuthProvider {
|
||||
public appDetector: LocalAppDetector
|
||||
|
||||
private authStatus: AuthStatus = defaultAuthStatus
|
||||
public webview?: Omit<vscode.Webview, 'postMessage'> & {
|
||||
postMessage(message: ExtensionMessage): Thenable<boolean>
|
||||
}
|
||||
|
||||
constructor(
|
||||
private config: Pick<ConfigurationWithAccessToken, 'serverEndpoint' | 'accessToken' | 'customHeaders'>,
|
||||
@ -38,18 +42,20 @@ export class AuthProvider {
|
||||
) {
|
||||
this.authStatus.endpoint = 'init'
|
||||
this.loadEndpointHistory()
|
||||
this.appDetector = new LocalAppDetector({ onChange: token => this.syncLocalAppState(token) })
|
||||
this.appDetector = new LocalAppDetector(secretStorage, { onChange: type => this.syncLocalAppState(type) })
|
||||
}
|
||||
|
||||
// Sign into the last endpoint the user was signed into
|
||||
// if none, try signing in with App URL
|
||||
public async init(): Promise<void> {
|
||||
const lastEndpoint = this.localStorage?.getEndpoint() || this.config.serverEndpoint
|
||||
debug('AuthProvider:init:lastEndpoint', lastEndpoint)
|
||||
const tokenKey = isLocalApp(lastEndpoint) ? 'SOURCEGRAPH_CODY_APP' : lastEndpoint
|
||||
const token = (await this.secretStorage.get(tokenKey)) || this.config.accessToken
|
||||
await this.auth(lastEndpoint, token || null)
|
||||
await this.appDetector.init()
|
||||
const lastEndpoint = this.localStorage?.getEndpoint() || this.config.serverEndpoint
|
||||
const token = (await this.secretStorage.get(lastEndpoint || '')) || this.config.accessToken
|
||||
debug('AuthProvider:init:lastEndpoint', lastEndpoint)
|
||||
const authState = await this.auth(lastEndpoint, token || null)
|
||||
if (authState?.isLoggedIn) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Display quickpick to select endpoint to sign in to
|
||||
@ -215,20 +221,24 @@ export class AuthProvider {
|
||||
}
|
||||
|
||||
public async announceNewAuthStatus(): Promise<void> {
|
||||
if (this.authStatus.endpoint === 'init') {
|
||||
if (this.authStatus.endpoint === 'init' || !this.webview) {
|
||||
return
|
||||
}
|
||||
await vscode.commands.executeCommand('cody.auth.sync', this.authStatus)
|
||||
await vscode.commands.executeCommand('cody.auth.sync')
|
||||
}
|
||||
|
||||
public async syncLocalAppState(token: string | null): Promise<void> {
|
||||
if (token) {
|
||||
await this.secretStorage.storeToken(LOCAL_APP_URL.href, token)
|
||||
if (!this.authStatus.isLoggedIn) {
|
||||
await this.appAuth()
|
||||
}
|
||||
/**
|
||||
* Display app state in webview view that is used during Signin flow
|
||||
*/
|
||||
public async syncLocalAppState(type: string): Promise<void> {
|
||||
if (this.authStatus.endpoint === 'init' || !this.webview) {
|
||||
return
|
||||
}
|
||||
await vscode.commands.executeCommand('cody.app.sync')
|
||||
// Log user into App if user is currently not logged in and has App running
|
||||
if (type !== 'app' && !this.authStatus.isLoggedIn) {
|
||||
await this.appAuth()
|
||||
}
|
||||
// Notify webview that app is installed
|
||||
await this.webview?.postMessage({ type: 'app-state', isInstalled: true })
|
||||
}
|
||||
|
||||
// Register URI Handler (vscode://sourcegraph.cody-ai) for:
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
import * as vscode from 'vscode'
|
||||
|
||||
import { version } from '../../package.json'
|
||||
import { LOCAL_APP_URL, LocalEnv } from '../chat/protocol'
|
||||
import { LOCAL_APP_URL, LocalEnv, isOsSupportedByApp } from '../chat/protocol'
|
||||
import { debug } from '../log'
|
||||
|
||||
import { AppJson, LOCAL_APP_LOCATIONS } from './LocalAppFsPaths'
|
||||
import { SecretStorage } from './SecretStorageProvider'
|
||||
|
||||
type OnChangeCallback = (token: string | null) => Promise<void>
|
||||
type OnChangeCallback = (type: string) => Promise<void>
|
||||
/**
|
||||
* Detects whether the user has the Sourcegraph app installed locally.
|
||||
*/
|
||||
export class LocalAppDetector implements vscode.Disposable {
|
||||
private localEnv: LocalEnv
|
||||
private token: string | null = null
|
||||
|
||||
// Check if the platform is supported and the user has a home directory
|
||||
private isSupported = false
|
||||
@ -24,12 +24,13 @@ export class LocalAppDetector implements vscode.Disposable {
|
||||
private _watchers: vscode.Disposable[] = []
|
||||
private onChange: OnChangeCallback
|
||||
|
||||
constructor(options: { onChange: OnChangeCallback }) {
|
||||
constructor(private secretStorage: SecretStorage, options: { onChange: OnChangeCallback }) {
|
||||
this.onChange = options.onChange
|
||||
this.localEnv = envInit
|
||||
this.localAppMarkers = LOCAL_APP_LOCATIONS[this.localEnv.os]
|
||||
// Only Mac is supported for now
|
||||
this.isSupported = this.localEnv.os === 'darwin' && this.localEnv.homeDir !== undefined
|
||||
this.isSupported =
|
||||
isOsSupportedByApp(this.localEnv.os, this.localEnv.arch) && this.localEnv.homeDir !== undefined
|
||||
}
|
||||
|
||||
public async getProcessInfo(isLoggedIn = false): Promise<LocalEnv> {
|
||||
@ -97,15 +98,13 @@ export class LocalAppDetector implements vscode.Disposable {
|
||||
const token = appJson.token
|
||||
// Once the token is found, we can stop watching the files
|
||||
if (token?.length) {
|
||||
debug('LocalAppDetector:fetchToken', 'found')
|
||||
this.localEnv.hasAppJson = true
|
||||
this.tokenFsPath = null
|
||||
this.token = token
|
||||
await this.found('token')
|
||||
await this.secretStorage.storeToken(LOCAL_APP_URL.href, token)
|
||||
await this.fetchServer()
|
||||
return
|
||||
}
|
||||
debug('LocalAppDetector:fetchToken', 'failed')
|
||||
debug('LocalAppDetector:fetchToken', 'found')
|
||||
}
|
||||
|
||||
// Check if App is running
|
||||
@ -119,8 +118,10 @@ export class LocalAppDetector implements vscode.Disposable {
|
||||
if (response.status === 200) {
|
||||
debug('LocalAppDetector:fetchServer', 'found')
|
||||
this.localEnv.isAppRunning = true
|
||||
await this.found('server', this.token)
|
||||
return
|
||||
await this.found('server')
|
||||
}
|
||||
if (!this.localEnv.hasAppJson) {
|
||||
await this.fetchToken()
|
||||
}
|
||||
} catch {
|
||||
debug('LocalAppDetector:fetchServer', 'failed')
|
||||
@ -128,12 +129,12 @@ export class LocalAppDetector implements vscode.Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the caller that the app has been found. send the token if it exists
|
||||
// Notify the caller that the app has been found
|
||||
// NOTE: Call this function only when the app is found
|
||||
private async found(type: 'app' | 'token' | 'server', token: string | null = null): Promise<void> {
|
||||
await this.onChange(token)
|
||||
debug('LocalAppDetector:found', type)
|
||||
private async found(type: 'app' | 'token' | 'server'): Promise<void> {
|
||||
this.localEnv.isAppInstalled = true
|
||||
await this.onChange(type)
|
||||
debug('LocalAppDetector:found', type)
|
||||
}
|
||||
|
||||
// We can dispose the file watcher when app is found or when user has logged in
|
||||
|
||||
@ -14,6 +14,13 @@ export const LOCAL_APP_LOCATIONS: LocalAppPaths = {
|
||||
hasToken: true,
|
||||
},
|
||||
],
|
||||
linux: [
|
||||
{
|
||||
dir: '~/.local/share/com.sourcegraph.cody/',
|
||||
file: 'app.json',
|
||||
hasToken: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export interface AppJson {
|
||||
|
||||
@ -27,13 +27,16 @@ export interface SecretStorage {
|
||||
|
||||
export class VSCodeSecretStorage implements SecretStorage {
|
||||
constructor(private secretStorage: vscode.SecretStorage) {}
|
||||
|
||||
// Catch corrupted token in secret storage
|
||||
public async get(key: string): Promise<string | undefined> {
|
||||
if (!key) {
|
||||
return undefined
|
||||
try {
|
||||
if (key) {
|
||||
return await this.secretStorage.get(key)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get token from Secret Storage', error)
|
||||
}
|
||||
const secret = await this.secretStorage.get(key)
|
||||
return secret
|
||||
return undefined
|
||||
}
|
||||
|
||||
public async store(key: string, value: string): Promise<void> {
|
||||
|
||||
@ -131,7 +131,7 @@ export const App: React.FunctionComponent<{ vscodeAPI: VSCodeWrapper }> = ({ vsc
|
||||
|
||||
return (
|
||||
<div className="outer-container">
|
||||
<Header />
|
||||
<Header endpoint={authStatus.isLoggedIn ? endpoint : null} />
|
||||
{view === 'login' || !authStatus.isLoggedIn ? (
|
||||
<Login
|
||||
authStatus={authStatus}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'
|
||||
|
||||
import { APP_CALLBACK_URL, APP_DOWNLOAD_URLS, APP_LANDING_URL } from '../src/chat/protocol'
|
||||
import { APP_CALLBACK_URL, APP_LANDING_URL, archConvertor } from '../src/chat/protocol'
|
||||
|
||||
import { VSCodeWrapper } from './utils/VSCodeApi'
|
||||
|
||||
@ -29,7 +29,10 @@ export const ConnectApp: React.FunctionComponent<ConnectAppProps> = ({
|
||||
const buttonText = inDownloadMode ? 'Download Cody App' : isAppRunning ? 'Connect Cody App' : 'Open Cody App'
|
||||
const buttonIcon = inDownloadMode ? 'cloud-download' : isAppRunning ? 'link' : 'rocket'
|
||||
// Open landing page if download link for user's arch cannot be found
|
||||
const DOWNLOAD_URL = APP_DOWNLOAD_URLS[appOS]?.[appArch] || APP_LANDING_URL.href
|
||||
const DOWNLOAD_URL =
|
||||
isOSSupported && appOS && appArch
|
||||
? `https://sourcegraph.com/.api/app/latest?arch=${archConvertor(appArch)}&target=${appOS}`
|
||||
: APP_LANDING_URL.href
|
||||
// If the user already has the app installed, open the callback URL directly.
|
||||
const callbackUri = new URL(APP_CALLBACK_URL.href)
|
||||
callbackUri.searchParams.append('requestFrom', callbackScheme === 'vscode-insiders' ? 'CODY_INSIDERS' : 'CODY')
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AuthStatus } from '../src/chat/protocol'
|
||||
import { AuthStatus, isLocalApp } from '../src/chat/protocol'
|
||||
|
||||
import styles from './Login.module.css'
|
||||
|
||||
@ -34,7 +34,7 @@ export const ErrorContainer: React.FunctionComponent<{
|
||||
return null
|
||||
}
|
||||
// Errors for app are handled in the ConnectApp component
|
||||
if (isApp.isInstalled && !isApp.isRunning) {
|
||||
if ((isLocalApp(endpoint || '') || isApp.isInstalled) && !isApp.isRunning) {
|
||||
return null
|
||||
}
|
||||
// new users will not have an endpoint
|
||||
|
||||
@ -4,9 +4,9 @@ import { CodyColoredSvg } from '@sourcegraph/cody-ui/src/utils/icons'
|
||||
|
||||
import styles from './Header.module.css'
|
||||
|
||||
export const Header: React.FunctionComponent = () => (
|
||||
export const Header: React.FunctionComponent<{ endpoint: string | null }> = ({ endpoint }) => (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.logo}>
|
||||
<div className={styles.logo} title={endpoint || 'Cody'}>
|
||||
<CodyColoredSvg />
|
||||
</div>
|
||||
<VSCodeTag className={styles.tag}>beta</VSCodeTag>
|
||||
|
||||
@ -3,7 +3,7 @@ import { useCallback } from 'react'
|
||||
import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { AuthStatus, DOTCOM_URL, LOCAL_APP_URL } from '../src/chat/protocol'
|
||||
import { AuthStatus, DOTCOM_URL, LOCAL_APP_URL, isOsSupportedByApp } from '../src/chat/protocol'
|
||||
|
||||
import { ConnectApp } from './ConnectApp'
|
||||
import { ErrorContainer } from './Error'
|
||||
@ -49,7 +49,7 @@ export const Login: React.FunctionComponent<React.PropsWithChildren<LoginProps>>
|
||||
isAppRunning = false,
|
||||
onLoginRedirect,
|
||||
}) => {
|
||||
const isOSSupported = appOS === 'darwin' && appArch === 'arm64'
|
||||
const isOSSupported = isOsSupportedByApp(appOS, appArch)
|
||||
|
||||
const onFooterButtonClick = useCallback(
|
||||
(title: 'signin' | 'support') => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user