diff --git a/client/cody-shared/src/chat/recipes/inline-chat.ts b/client/cody-shared/src/chat/recipes/inline-chat.ts index 05e378ccb07..09378c7a0b1 100644 --- a/client/cody-shared/src/chat/recipes/inline-chat.ts +++ b/client/cody-shared/src/chat/recipes/inline-chat.ts @@ -15,15 +15,12 @@ export class InlineChat implements Recipe { public async getInteraction(humanChatInput: string, context: RecipeContext): Promise { const selection = context.editor.controller?.selection if (!humanChatInput || !selection) { - await context.editor.showWarningMessage( - 'Failed to start Inline Assist. Please highlight a small section of code in your file to try again.' - ) + await context.editor.showWarningMessage('Failed to start Inline Chat: empty input or selection.') return null } - - // Redirect fix-up requests - if (humanChatInput.startsWith('/fix ') || humanChatInput.startsWith('/f ')) { - return new Fixup().getInteraction(humanChatInput.replace('/fix ', '').replace('/f ', ''), context) + // Check if this is a fix-up request + if (/^\/f(ix)?\s/i.test(humanChatInput)) { + return new Fixup().getInteraction(humanChatInput.replace(/^\/f(ix)?\s/i, ''), context) } const truncatedText = truncateText(humanChatInput, MAX_HUMAN_INPUT_TOKENS) diff --git a/client/cody-shared/src/configuration.ts b/client/cody-shared/src/configuration.ts index 4115f78582d..28c84c080eb 100644 --- a/client/cody-shared/src/configuration.ts +++ b/client/cody-shared/src/configuration.ts @@ -1,7 +1,6 @@ export type ConfigurationUseContext = 'embeddings' | 'keyword' | 'none' | 'blended' export interface Configuration { - enabled: boolean serverEndpoint: string codebase?: string debug: boolean diff --git a/client/cody-ui/src/chat/inputContext/ChatInputContext.module.css b/client/cody-ui/src/chat/inputContext/ChatInputContext.module.css index 1e87412d10f..a80ed38f52e 100644 --- a/client/cody-ui/src/chat/inputContext/ChatInputContext.module.css +++ b/client/cody-ui/src/chat/inputContext/ChatInputContext.module.css @@ -47,11 +47,18 @@ overflow: hidden; } -.fail { - display: inline-flex; - align-items: center; - gap: 0.25rem; - overflow: hidden; - - color: var(--vscode-errorForeground, red); +.index-present, +.index-missing { + font-weight: bold; +} + +.indexStatusOnHover, +a:hover .indexStatus { + display: none; +} + +.indexStatus, +a:hover .indexStatusOnHover { + display: inline; + color: var(--vscode-list-warningForeground, #fdf1cc); } diff --git a/client/cody-ui/src/chat/inputContext/ChatInputContext.tsx b/client/cody-ui/src/chat/inputContext/ChatInputContext.tsx index 7624b88c0ae..e85b4b5e161 100644 --- a/client/cody-ui/src/chat/inputContext/ChatInputContext.tsx +++ b/client/cody-ui/src/chat/inputContext/ChatInputContext.tsx @@ -10,6 +10,9 @@ import { Icon } from '../../utils/Icon' import styles from './ChatInputContext.module.css' +const warning = + 'This repository has not yet been configured for Cody indexing on Sourcegraph, and response quality will be poor. To enable Cody’s code graph indexing, click here to see the Cody documentation, or email support@sourcegraph.com for assistance.' + export const ChatInputContext: React.FunctionComponent<{ contextStatus: ChatContextStatus className?: string @@ -21,7 +24,7 @@ export const ChatInputContext: React.FunctionComponent<{ ? { icon: contextStatus.connection ? mdiSourceRepository : mdiFileExcel, text: basename(contextStatus.codebase.replace(/^(github|gitlab)\.com\//, '')), - tooltip: contextStatus.connection ? contextStatus.codebase : 'connection failed', + tooltip: contextStatus.connection ? contextStatus.codebase : warning, } : null, contextStatus.filePath @@ -38,9 +41,19 @@ export const ChatInputContext: React.FunctionComponent<{ return (
{contextStatus.mode && contextStatus.connection ? ( -

Embeddings

+

+ Indexed +

) : contextStatus.supportsKeyword ? ( -

Keyword

+

+ + ⚠ Not Indexed + Generate Index + +

) : null} {items.length > 0 && ( @@ -61,7 +74,7 @@ const ContextItem: React.FunctionComponent<{ icon: string; text: string; tooltip tooltip, as: Tag, }) => ( - + {text} diff --git a/client/cody/CHANGELOG.md b/client/cody/CHANGELOG.md index d6332eb077d..4a421966826 100644 --- a/client/cody/CHANGELOG.md +++ b/client/cody/CHANGELOG.md @@ -8,8 +8,12 @@ All notable changes to Sourcegraph Cody will be documented in this file. ### Fixed +- Displays error banners on all view instead of chat view only. [pull/51883](https://github.com/sourcegraph/sourcegraph/pull/51883) +- Surfaces errors for corrupted token from secret storage. [pull/51883](https://github.com/sourcegraph/sourcegraph/pull/51883) + ### Changed +- Removes unused configuration option: `cody.enabled`. [pull/51883](https://github.com/sourcegraph/sourcegraph/pull/51883) - Arrow key behavior: you can now navigate forwards through messages with the down arrow; additionally the up and down arrows will navigate backwards and forwards only if you're at the start or end of the drafted text, respectively. [pull/51586](https://github.com/sourcegraph/sourcegraph/pull/51586) ## [0.1.2] diff --git a/client/cody/integration-test/helpers.ts b/client/cody/integration-test/helpers.ts index 7bf4e290a5f..063d581974e 100644 --- a/client/cody/integration-test/helpers.ts +++ b/client/cody/integration-test/helpers.ts @@ -26,7 +26,6 @@ export async function beforeIntegrationTest(): Promise { // Configure extension. const config = vscode.workspace.getConfiguration() await config.update('cody.serverEndpoint', `http://localhost:${mockServer.SERVER_PORT}`) - await config.update('cody.enabled', true) await ensureExecuteCommand('cody.set-access-token', ['test-token']) } diff --git a/client/cody/package.json b/client/cody/package.json index a8bf1d6591c..68853f55be4 100644 --- a/client/cody/package.json +++ b/client/cody/package.json @@ -93,10 +93,6 @@ ] }, "commands": [ - { - "command": "cody.toggle-enabled", - "title": "Cody: Toggle Enabled/Disabled" - }, { "command": "cody.recipe.explain-code", "title": "Ask Cody: Explain Code in Detail" @@ -170,17 +166,20 @@ { "command": "cody.comment.add", "title": "Ask Cody", + "category": "Cody Inline Assist", "enablement": "!commentIsEmpty" }, { "command": "cody.comment.delete", - "title": "Cody: Remove chat", + "title": "Cody Inline Assist: Remove Comment", + "category": "Cody Inline Assist", "enablement": "!commentThreadIsEmpty", "icon": "$(trash)" }, { "command": "cody.comment.load", - "title": "Cody: Loading", + "title": "Cody Inline Assist: Loading", + "category": "Cody Inline Assist", "enablement": "!commentThreadIsEmpty", "icon": "$(sync~spin)" } @@ -322,6 +321,11 @@ "command": "cody.comment.add", "group": "inline", "when": "cody.activated && commentController =~ /^cody-inline/" + }, + { + "command": "cody.focus", + "group": "inline", + "when": "!cody.activated && commentController =~ /^cody-inline/" } ], "comments/commentThread/title": [ @@ -341,27 +345,21 @@ "type": "object", "title": "Cody", "properties": { - "cody.enabled": { - "order": 1, - "type": "boolean", - "default": true, - "description": "Enable Cody" - }, "cody.serverEndpoint": { - "order": 2, + "order": 1, "type": "string", "default": "https://sourcegraph.com", "example": "https://example.sourcegraph.com", "description": "URL to the Sourcegraph instance." }, "cody.codebase": { - "order": 3, + "order": 2, "type": "string", - "markdownDescription": "Repo path that cody will use to gather context for the answers. Example: 'github.com/sourcegraph/sourcegraph' . This is automatically inferred from your git setup but you can use this option if you need to overwrite the default", + "markdownDescription": "The name of the embedded repository that Cody will use to gather context for its responses. This is automatically inferred from your Git metadata but you can use this option if you need to override the default.", "example": "github.com/sourcegraph/sourcegraph" }, "cody.useContext": { - "order": 4, + "order": 3, "type": "string", "enum": [ "embeddings", @@ -370,24 +368,24 @@ "blended" ], "default": "embeddings", - "markdownDescription": "If embeddings for a repo are present, Cody will use them to set the context for Sourcegraph search (best scneario). If not, it will automatically fall back to keyword based search" + "markdownDescription": "If 'embeddings' is selected, Cody will prefer to use an embeddings-based index when fetching context to generate responses to user requests. If no such index is found, it will fall back to using keyword-based local context fetching. If 'keyword' is selected, Cody will use keyword context." }, "cody.experimental.suggestions": { - "order": 5, + "order": 4, "type": "boolean", - "markdownDescription": "Enables code completions while typing in the code editor window", + "markdownDescription": "Enables Cody inline autocompletion in your editor.", "default": false }, "cody.experimental.chatPredictions": { - "order": 6, + "order": 5, "type": "boolean", "default": false, - "markdownDescription": "Adds sugestions of possible relevant messages in the chat window" + "markdownDescription": "Adds sugestions of possible relevant messages in the chat window." }, "cody.experimental.inline": { - "order": 7, + "order": 6, "type": "boolean", - "markdownDescription": "Enables inline chat with cody inside the code editor window", + "markdownDescription": "Enables Cody Inline Assist, an inline way to explicitly ask questions and propose modifications to code.", "default": false }, "cody.experimental.connectToApp": { @@ -395,9 +393,9 @@ "default": false }, "cody.customHeaders": { - "order": 8, + "order": 7, "type": "object", - "markdownDescription": "Adds custom HTTP headers to all network requests. Usefull if you are behind a proxy server that requires custom headers", + "markdownDescription": "Adds custom HTTP headers to all network requests to the Sourcegraph endpoint. Defining required headers here ensures requests are properly forwarded through intermediary proxy servers, which may mandate certain custom headers for internal or external communication.", "default": {}, "examples": [ { @@ -409,7 +407,7 @@ "cody.debug": { "order": 99, "type": "boolean", - "markdownDescription": "(Only relevant for the team developing and improving Cody)" + "markdownDescription": "Adds 'Debug' view to Cody Chat view that allows developers to see and log errors." } } } diff --git a/client/cody/src/chat/ChatViewProvider.ts b/client/cody/src/chat/ChatViewProvider.ts index 97f76be3abc..b1baef1a9d6 100644 --- a/client/cody/src/chat/ChatViewProvider.ts +++ b/client/cody/src/chat/ChatViewProvider.ts @@ -207,8 +207,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp this.sendEvent(message.event, message.value) break case 'removeToken': - await this.secretStorage.delete(CODY_ACCESS_TOKEN_SECRET) - this.sendEvent('token', 'Delete') + await this.logout() break case 'removeHistory': await this.clearHistory() @@ -325,14 +324,11 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp } private async executeChatCommands(text: string): Promise { - const command = text.split(' ')[0] - switch (command) { - case '/reset': - case '/r': + switch (true) { + case /^\/r(est)?\s/i.test(text): await this.clearAndRestartSession() break - case '/search': - case '/s': + case /^\/s(earch)?\s/i.test(text): await this.executeRecipe('context-search', text) break default: @@ -523,6 +519,18 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp void this.webview?.postMessage({ type: 'login', isValid }) } + /** + * Logout deletes token from secret storage + * Also removes the avtivate status for the extension to disable access to all commands and set webview back to login view + */ + public async logout(): Promise { + await this.secretStorage.delete(CODY_ACCESS_TOKEN_SECRET) + await vscode.commands.executeCommand('setContext', 'cody.activated', false) + this.sendEvent('token', 'Delete') + this.sendEvent('auth', 'logout') + this.setWebviewView('login') + } + /** * Loads chat history from local storage */ diff --git a/client/cody/src/configuration.test.ts b/client/cody/src/configuration.test.ts index cc052c2abc9..321d658ad56 100644 --- a/client/cody/src/configuration.test.ts +++ b/client/cody/src/configuration.test.ts @@ -8,7 +8,6 @@ describe('getConfiguration', () => { get: (_key: string, defaultValue?: T): typeof defaultValue | undefined => defaultValue, } expect(getConfiguration(config)).toEqual({ - enabled: true, serverEndpoint: '', codebase: '', debug: false, @@ -24,8 +23,6 @@ describe('getConfiguration', () => { const config: Pick = { get: key => { switch (key) { - case 'cody.enabled': - return false case 'cody.serverEndpoint': return 'http://example.com' case 'cody.codebase': @@ -51,7 +48,6 @@ describe('getConfiguration', () => { }, } expect(getConfiguration(config)).toEqual({ - enabled: false, serverEndpoint: 'http://example.com', codebase: 'my/codebase', debug: true, diff --git a/client/cody/src/configuration.ts b/client/cody/src/configuration.ts index f4b22cfcae1..bd468860726 100644 --- a/client/cody/src/configuration.ts +++ b/client/cody/src/configuration.ts @@ -13,7 +13,6 @@ import { SecretStorage, getAccessToken } from './services/SecretStorageProvider' */ export function getConfiguration(config: Pick): Configuration { return { - enabled: config.get('cody.enabled', true), serverEndpoint: sanitizeServerEndpoint(config.get('cody.serverEndpoint', '')), codebase: sanitizeCodebase(config.get('cody.codebase')), debug: config.get('cody.debug', false), diff --git a/client/cody/src/main.ts b/client/cody/src/main.ts index 40868e2c916..6d7073bddb0 100644 --- a/client/cody/src/main.ts +++ b/client/cody/src/main.ts @@ -72,7 +72,7 @@ const register = async ( }> => { const disposables: vscode.Disposable[] = [] - await updateEventLogger(initialConfig, localStorage) + void updateEventLogger(initialConfig, localStorage) // Controller for inline assist const commentController = new InlineController(context.extensionPath) @@ -107,9 +107,9 @@ const register = async ( disposables.push( vscode.window.registerWebviewViewProvider('cody.chat', chatProvider, { webviewOptions: { retainContextWhenHidden: true }, - }) + }), + { dispose: () => vscode.commands.executeCommand('setContext', 'cody.activated', false) } ) - disposables.push({ dispose: () => vscode.commands.executeCommand('setContext', 'cody.activated', false) }) const executeRecipe = async (recipe: RecipeID): Promise => { await vscode.commands.executeCommand('cody.chat.focus') @@ -139,7 +139,7 @@ const register = async ( disposables.push( // File Chat Provider vscode.commands.registerCommand('cody.comment.add', async (comment: vscode.CommentReply) => { - const isFixMode = comment.text.trimStart().startsWith('/f') + const isFixMode = /^\/f(ix)?\s/i.test(comment.text.trimStart()) await commentController.chat(comment, isFixMode) await chatProvider.executeRecipe(isFixMode ? 'fixup' : 'inline-chat', comment.text, false) logEvent(`CodyVSCodeExtension:inline-assist:${isFixMode ? 'fixup' : 'chat'}`) @@ -147,25 +147,14 @@ const register = async ( vscode.commands.registerCommand('cody.comment.delete', (thread: vscode.CommentThread) => { commentController.delete(thread) }), - // Toggle Chat - vscode.commands.registerCommand('cody.toggle-enabled', async () => { - await workspaceConfig.update( - 'cody.enabled', - !workspaceConfig.get('cody.enabled'), - vscode.ConfigurationTarget.Global - ) - logEvent('CodyVSCodeExtension:codyToggleEnabled:clicked') - }), - // Access token - // This is only used in configuration tests + // Access token - this is only used in configuration tests vscode.commands.registerCommand('cody.set-access-token', async (args: any[]) => { if (args?.length && (args[0] as string)) { await secretStorage.store(CODY_ACCESS_TOKEN_SECRET, args[0]) } }), vscode.commands.registerCommand('cody.delete-access-token', async () => { - await secretStorage.delete(CODY_ACCESS_TOKEN_SECRET) - logEvent('CodyVSCodeExtension:codyDeleteAccessToken:clicked') + await chatProvider.logout() }), // Commands vscode.commands.registerCommand('cody.focus', () => vscode.commands.executeCommand('cody.chat.focus')), @@ -173,7 +162,6 @@ const register = async ( vscode.commands.registerCommand('cody.history', () => chatProvider.setWebviewView('history')), vscode.commands.registerCommand('cody.interactive.clear', async () => { await chatProvider.clearAndRestartSession() - chatProvider.setWebviewView('chat') }), vscode.commands.registerCommand('cody.recipe.explain-code', () => executeRecipe('explain-code-detailed')), vscode.commands.registerCommand('cody.recipe.explain-code-high-level', () => @@ -233,10 +221,8 @@ const register = async ( vscode.commands.registerCommand('cody.experimental.suggest', async () => { await completionsProvider.fetchAndShowCompletions() }), - vscode.commands.registerCommand('cody.completions.inline.accepted', (...args) => { - const params = { - type: 'inline', - } + vscode.commands.registerCommand('cody.completions.inline.accepted', () => { + const params = { type: 'inline' } logEvent('CodyVSCodeExtension:completion:accepted', params, params) }), vscode.languages.registerInlineCompletionItemProvider({ scheme: 'file' }, completionsProvider) diff --git a/client/cody/src/services/InlineController.ts b/client/cody/src/services/InlineController.ts index 417b6806571..5dc261510ef 100644 --- a/client/cody/src/services/InlineController.ts +++ b/client/cody/src/services/InlineController.ts @@ -44,7 +44,7 @@ export class InlineController { constructor(private extensionPath: string) { this.commentController = vscode.comments.createCommentController(this.id, this.label) this.commentController.options = this.options - // Track last selection in valid doc + // Track last selection range in valid doc before an action is called vscode.window.onDidChangeTextEditorSelection(e => { if (e.textEditor.document.uri.scheme !== 'file') { return @@ -58,9 +58,9 @@ export class InlineController { this.selectionRange = range } }) - // Track and update line of changes when the task for the current selected range is being processed + // Track and update line diff when a task for the current selected range is being processed (this.isInProgress) + // This makes sure the comment range and highlights are also updated correctly vscode.workspace.onDidChangeTextDocument(e => { - // don't track if (!this.isInProgress || !this.selectionRange || e.document.uri.scheme !== 'file') { return } @@ -71,7 +71,7 @@ export class InlineController { }) } /** - * Getter + * Getter to return instance */ public get(): vscode.CommentController { return this.commentController @@ -321,6 +321,9 @@ export class Comment implements vscode.Comment { } } +/** + * For tracking lines diff + */ export function lineTracker(e: vscode.TextDocumentChangeEvent, cur: vscode.Range): vscode.Range | null { for (const change of e.contentChanges) { if (change.range.start.line > cur.end.line) { @@ -340,13 +343,15 @@ export function lineTracker(e: vscode.TextDocumentChangeEvent, cur: vscode.Range } return null } - +/** + * Create selection range for a single line + * This is used for display the Cody icon and Code action on top of the first line of selected code + */ export function singleLineRange(line: number): vscode.Range { return new vscode.Range(line, 0, line, 0) } - /** - * Generate icon path for each speaker + * Generate icon path for each speaker: cody vs human (sourcegraph) */ export function getIconPath(speaker: string, extPath: string): vscode.Uri { const extensionPath = vscode.Uri.file(extPath) diff --git a/client/cody/src/services/SecretStorageProvider.ts b/client/cody/src/services/SecretStorageProvider.ts index 16f60f96bd4..f60532f4335 100644 --- a/client/cody/src/services/SecretStorageProvider.ts +++ b/client/cody/src/services/SecretStorageProvider.ts @@ -6,32 +6,37 @@ export async function getAccessToken(secretStorage: SecretStorage): Promise - store(key: string, value: string): Thenable - delete(key: string): Thenable + get(key: string): Promise + store(key: string, value: string): Promise + delete(key: string): Promise onDidChange(callback: (key: string) => Promise): vscode.Disposable } export class VSCodeSecretStorage implements SecretStorage { constructor(private secretStorage: vscode.SecretStorage) {} - public get(key: string): Thenable { - return this.secretStorage.get(key) + public async get(key: string): Promise { + const secret = await this.secretStorage.get(key) + return secret } - public store(key: string, value: string): Thenable { - return this.secretStorage.store(key, value) + public async store(key: string, value: string): Promise { + if (value && value.length > 8) { + await this.secretStorage.store(key, value) + } } - public delete(key: string): Thenable { - return this.secretStorage.delete(key) + public async delete(key: string): Promise { + await this.secretStorage.delete(key) } public onDidChange(callback: (key: string) => Promise): vscode.Disposable { @@ -48,11 +53,15 @@ export class InMemorySecretStorage implements SecretStorage { this.callbacks = [] } - public get(key: string): Thenable { + public async get(key: string): Promise { return Promise.resolve(this.storage.get(key)) } - public store(key: string, value: string): Thenable { + public async store(key: string, value: string): Promise { + if (!value) { + return + } + this.storage.set(key, value) for (const cb of this.callbacks) { @@ -63,7 +72,7 @@ export class InMemorySecretStorage implements SecretStorage { return Promise.resolve() } - public delete(key: string): Thenable { + public async delete(key: string): Promise { this.storage.delete(key) for (const cb of this.callbacks) { diff --git a/client/cody/webviews/App.tsx b/client/cody/webviews/App.tsx index fee10c44211..365fc447c80 100644 --- a/client/cody/webviews/App.tsx +++ b/client/cody/webviews/App.tsx @@ -126,6 +126,7 @@ export const App: React.FunctionComponent<{ vscodeAPI: VSCodeWrapper }> = ({ vsc enableConnectToApp={config?.experimentalConnectToApp} /> )} + {errorMessages && } {view !== 'login' && } {view === 'debug' && config?.debug && } {view === 'history' && ( @@ -139,7 +140,6 @@ export const App: React.FunctionComponent<{ vscodeAPI: VSCodeWrapper }> = ({ vsc )} {view === 'recipes' && } {view === 'settings' && } - {view === 'chat' && errorMessages && } {view === 'chat' && (