From 1a0e0ca731680712be59ec6412e2dc92ac87398f Mon Sep 17 00:00:00 2001 From: Tom Ross Date: Tue, 1 Aug 2023 10:18:10 +0100 Subject: [PATCH] Inline Fix: Improve response quality (#376) --- agent/src/editor.ts | 5 + cli/src/client/index.ts | 4 +- lib/shared/src/chat/client.ts | 2 +- lib/shared/src/chat/recipes/fixup.ts | 207 ++++++++++++++++++---- lib/shared/src/chat/useClient.ts | 2 +- lib/shared/src/editor/index.ts | 18 ++ lib/shared/src/intent-detector/client.ts | 114 +++++++++++- lib/shared/src/intent-detector/index.ts | 23 +++ lib/shared/src/prompt/templates.ts | 22 +++ lib/shared/src/test/mocks.ts | 25 ++- vscode/CHANGELOG.md | 3 + vscode/src/chat/InlineChatViewProvider.ts | 5 + vscode/src/editor/vscode-editor.ts | 40 +++++ vscode/src/external-services.ts | 2 +- vscode/test/fixtures/mock-server.ts | 11 +- vscode/webviews/Recipes.tsx | 11 +- web/src/App.tsx | 3 + 17 files changed, 450 insertions(+), 47 deletions(-) diff --git a/agent/src/editor.ts b/agent/src/editor.ts index eedc40e1925e..aac2dfa80f9c 100644 --- a/agent/src/editor.ts +++ b/agent/src/editor.ts @@ -2,6 +2,7 @@ import { URI } from 'vscode-uri' import { ActiveTextEditor, + ActiveTextEditorDiagnostic, ActiveTextEditorSelection, ActiveTextEditorViewControllers, ActiveTextEditorVisibleContent, @@ -78,6 +79,10 @@ export class AgentEditor implements Editor { return this.getActiveTextEditorSelection() } + public getActiveTextEditorDiagnosticsForRange(): ActiveTextEditorDiagnostic[] | null { + throw new Error('Method not implemented.') + } + public getActiveTextEditorVisibleContent(): ActiveTextEditorVisibleContent | null { const document = this.activeDocument() if (document === undefined) { diff --git a/cli/src/client/index.ts b/cli/src/client/index.ts index 06c994a37c54..5c319eab2fb7 100644 --- a/cli/src/client/index.ts +++ b/cli/src/client/index.ts @@ -49,8 +49,6 @@ export async function getClient({ codebase, endpoint, context: contextType, debu process.exit(1) } - const intentDetector = new SourcegraphIntentDetectorClient(sourcegraphClient) - const completionsClient = new SourcegraphNodeCompletionsClient({ serverEndpoint: endpoint, accessToken, @@ -58,5 +56,7 @@ export async function getClient({ codebase, endpoint, context: contextType, debu customHeaders: {}, }) + const intentDetector = new SourcegraphIntentDetectorClient(sourcegraphClient, completionsClient) + return { codebaseContext, intentDetector, completionsClient } } diff --git a/lib/shared/src/chat/client.ts b/lib/shared/src/chat/client.ts index 7ce3e187d89b..7ba9682912f7 100644 --- a/lib/shared/src/chat/client.ts +++ b/lib/shared/src/chat/client.ts @@ -89,7 +89,7 @@ export async function createClient({ const codebaseContext = new CodebaseContext(config, config.codebase, embeddingsSearch, null, null) - const intentDetector = new SourcegraphIntentDetectorClient(graphqlClient) + const intentDetector = new SourcegraphIntentDetectorClient(graphqlClient, completionsClient) const transcript = initialTranscript || new Transcript() diff --git a/lib/shared/src/chat/recipes/fixup.ts b/lib/shared/src/chat/recipes/fixup.ts index 1d2a9fbc3dbd..4b030de29912 100644 --- a/lib/shared/src/chat/recipes/fixup.ts +++ b/lib/shared/src/chat/recipes/fixup.ts @@ -1,13 +1,54 @@ -import { CodebaseContext } from '../../codebase-context' -import { ContextMessage } from '../../codebase-context/messages' +import { ContextMessage, getContextMessageWithResponse } from '../../codebase-context/messages' +import { ActiveTextEditorSelection } from '../../editor' +import { IntentClassificationOption } from '../../intent-detector' import { MAX_CURRENT_FILE_TOKENS, MAX_HUMAN_INPUT_TOKENS } from '../../prompt/constants' +import { populateCodeContextTemplate, populateCurrentEditorDiagnosticsTemplate } from '../../prompt/templates' import { truncateText, truncateTextStart } from '../../prompt/truncation' import { BufferedBotResponseSubscriber } from '../bot-response-multiplexer' import { Interaction } from '../transcript/interaction' -import { contentSanitizer, numResults } from './helpers' +import { contentSanitizer, getContextMessagesFromSelection } from './helpers' import { Recipe, RecipeContext, RecipeID } from './recipe' +type FixupIntent = 'add' | 'edit' | 'delete' | 'fix' | 'test' | 'document' +const FixupIntentClassification: IntentClassificationOption[] = [ + { + id: 'add', + description: 'Add to the selected code', + examplePrompts: ['Add a function that concatonates two strings', 'Add error handling'], + }, + { + id: 'edit', + description: 'Edit part of the selected code', + examplePrompts: ['Edit this code', 'Change this code', 'Update this code'], + }, + { + id: 'delete', + description: 'Delete a part of the selection code', + examplePrompts: ['Delete these comments', 'Remove log statements'], + }, + { + id: 'fix', + description: 'Fix a problem in a part of the selected code', + examplePrompts: ['Implement this TODO', 'Fix this code'], + }, + { + id: 'document', + description: 'Generate documentation for parts of the selected code.', + examplePrompts: ['Add a docstring for this function', 'Write comments to explain this code'], + }, +] + +const PromptIntentInstruction: Record = { + add: 'The user wants you to add to the selected code by following their instructions.', + edit: 'The user wants you to replace parts of the selected code by following their instructions.', + delete: 'The user wants you to remove parts of the selected code by following their instructions.', + fix: 'The user wants you to correct a problem in the selected code by following their instructions.', + document: + 'The user wants you to add documentation or comments to the selected code by following their instructions.', + test: 'The user wants you to add, update or fix a test by following their instructions', +} + export class Fixup implements Recipe { public id: RecipeID = 'fixup' @@ -27,15 +68,22 @@ export class Fixup implements Recipe { return null } - // Reconstruct Cody's prompt using user's context + const intent = await this.getIntent(humanChatInput, context) + + // It is possible to trigger this recipe from the sidebar without any input. + // TODO: Consider deprecating this functionality once inline fixups and non-stop fixups and consolidated. + const promptInstruction = + humanChatInput.length > 0 + ? truncateText(humanChatInput, MAX_HUMAN_INPUT_TOKENS) + : "You should infer your instructions from the users' selection" + + // Reconstruct Cody's prompt using user's context and intent // Replace placeholders in reverse order to avoid collisions if a placeholder occurs in the input // TODO: Move prompt suffix from recipe to chat view. It has other subscribers. const promptText = Fixup.prompt - .replace('{humanInput}', truncateText(humanChatInput, MAX_HUMAN_INPUT_TOKENS)) - .replace('{responseMultiplexerPrompt}', context.responseMultiplexer.prompt()) - .replace('{truncateFollowingText}', truncateText(selection.followingText, quarterFileContext)) + .replace('{humanInput}', promptInstruction) + .replace('{intent}', PromptIntentInstruction[intent]) .replace('{selectedText}', selection.selectedText) - .replace('{truncateTextStart}', truncateTextStart(selection.precedingText, quarterFileContext)) .replace('{fileName}', selection.fileName) context.responseMultiplexer.sub( @@ -67,36 +115,133 @@ export class Fixup implements Recipe { speaker: 'assistant', prefix: 'Check your document for updates from Cody.\n', }, - this.getContextMessages(selection.selectedText, context.codebaseContext), + this.getContextFromIntent(intent, selection, quarterFileContext, context), [] ) ) } - // Get context from editor - private async getContextMessages(text: string, codebaseContext: CodebaseContext): Promise { - const contextMessages: ContextMessage[] = await codebaseContext.getContextMessages(text, numResults) - return contextMessages + private async getIntent(humanChatInput: string, context: RecipeContext): Promise { + /** + * TODO(umpox): We should probably find a shorter way of detecting intent when possible. + * Possible methods: + * - Input -> Match first word against update|fix|add|delete verbs + * - Context -> Infer intent from context, e.g. Current file is a test -> Test intent, Current selection is a comment symbol -> Documentation intent + */ + const intent = await context.intentDetector.classifyIntentFromOptions( + humanChatInput, + FixupIntentClassification, + 'edit' + ) + return intent + } + + private async getContextFromIntent( + intent: FixupIntent, + selection: ActiveTextEditorSelection, + quarterFileContext: number, + context: RecipeContext + ): Promise { + const truncatedPrecedingText = truncateTextStart(selection.precedingText, quarterFileContext) + const truncatedFollowingText = truncateText(selection.followingText, quarterFileContext) + + // Disable no case declarations because we get better type checking with a switch case + /* eslint-disable no-case-declarations */ + switch (intent) { + /** + * Intents that are focused on producing new code. + * They have a broad set of possible instructions, so we fetch a broad amount of code context files. + * Non-code files are not considered as including Markdown syntax seems to lead to more hallucinations and poorer output quality. + * + * TODO(umpox): We fetch similar context for both cases here + * We should investigate how we can improve each individual case. + * Are these fundamentally the same? Is the primary benefit here that we can provide more specific instructions to Cody? + */ + case 'add': + case 'edit': + return getContextMessagesFromSelection( + selection.selectedText, + truncatedPrecedingText, + truncatedFollowingText, + selection, + context.codebaseContext + ) + /** + * The fix intent is similar to adding or editing code, but with additional context that we can include from the editor. + */ + case 'fix': + const range = + context.editor.getActiveTextEditor()?.selectionRange || + context.editor.controllers?.inline?.selectionRange + const diagnostics = range ? context.editor.getActiveTextEditorDiagnosticsForRange(range) || [] : [] + const errorsAndWarnings = diagnostics.filter(({ type }) => type === 'error' || type === 'warning') + + return getContextMessagesFromSelection( + selection.selectedText, + truncatedPrecedingText, + truncatedFollowingText, + selection, + context.codebaseContext + ).then(messages => + messages.concat( + errorsAndWarnings.flatMap(diagnostic => + getContextMessageWithResponse( + populateCurrentEditorDiagnosticsTemplate(diagnostic, selection.fileName), + selection + ) + ) + ) + ) + /** + * The test intent is unique in that we likely want to be much more specific in that context that we retrieve. + * TODO(umpox): How can infer the current testing dependencies, etc? + */ + case 'test': + // Currently the same as add|edit|fix + return getContextMessagesFromSelection( + selection.selectedText, + truncatedPrecedingText, + truncatedFollowingText, + selection, + context.codebaseContext + ) + /** + * Intents that are focused primarily on updating code within the current file and selection. + * Providing a much more focused context window here seems to provide better quality responses. + */ + case 'delete': + case 'document': + return Promise.resolve( + [truncatedPrecedingText, truncatedFollowingText].flatMap(text => + getContextMessageWithResponse( + populateCodeContextTemplate(text, selection.fileName, selection.repoName), + selection + ) + ) + ) + } + /* eslint-enable no-case-declarations */ } // Prompt Templates public static readonly prompt = ` - This is part of the file {fileName}. The part of the file I have selected is highlighted with tags. You are helping me to work on that part as my coding assistant. - Follow the instructions in the selected part plus the additional instructions to produce a rewritten replacement for only the selected part. - Put the rewritten replacement inside tags. I only want to see the code within . - Do not move code from outside the selection into the selection in your reply. - Do not remove code inside the tags that might be being used by the code outside the tags. - It is OK to provide some commentary within the replacement . - It is not acceptable to enclose the rewritten replacement with markdowns. - Only provide me with the replacement and nothing else. - If it doesn't make sense, you do not need to provide . Instead, tell me how I can help you to understand my request. - - \`\`\` - {truncateTextStart}{selectedText}{truncateFollowingText} - \`\`\` - - Additional Instruction: - - {humanInput} - - {responseMultiplexerPrompt} -` +- You are an AI programming assistant who is an expert in updating code to meet given instructions. +- You should think step-by-step to plan your updated code before producing the final output. +- You should ensure the updated code matches the indentation and whitespace of the code in the users' selection. +- Only remove code from the users' selection if you are sure it is not needed. +- It is not acceptable to use Markdown in your response. You should not produce Markdown-formatted code blocks. +- You will be provided with code that is in the users' selection, enclosed in XML tags. You must use this code to help you plan your updated code. +- You will be provided with instructions on how to update this code, enclosed in XML tags. You must follow these instructions carefully and to the letter. +- Enclose your response in XML tags. Do not provide anything else. + +This is part of the file {fileName}. + +The user has the following code in their selection: +{selectedText} + +{intent} +Provide your generated code using the following instructions: + +{humanInput} +` } diff --git a/lib/shared/src/chat/useClient.ts b/lib/shared/src/chat/useClient.ts index f7f9e8198c75..3cda5f3d3fa3 100644 --- a/lib/shared/src/chat/useClient.ts +++ b/lib/shared/src/chat/useClient.ts @@ -134,7 +134,7 @@ export const useClient = ({ const completionsClient = new SourcegraphBrowserCompletionsClient(config) const chatClient = new ChatClient(completionsClient) const graphqlClient = new SourcegraphGraphQLAPIClient(config) - const intentDetector = new SourcegraphIntentDetectorClient(graphqlClient) + const intentDetector = new SourcegraphIntentDetectorClient(graphqlClient, completionsClient) return { graphqlClient, chatClient, intentDetector } }, [config]) diff --git a/lib/shared/src/editor/index.ts b/lib/shared/src/editor/index.ts index 003b5d716197..a53ef928fa5b 100644 --- a/lib/shared/src/editor/index.ts +++ b/lib/shared/src/editor/index.ts @@ -28,6 +28,15 @@ export interface ActiveTextEditorSelection { followingText: string } +export type ActiveTextEditorDiagnosticType = 'error' | 'warning' | 'information' | 'hint' + +export interface ActiveTextEditorDiagnostic { + type: ActiveTextEditorDiagnosticType + range: ActiveTextEditorSelectionRange + text: string + message: string +} + export interface ActiveTextEditorVisibleContent { content: string fileName: string @@ -37,6 +46,7 @@ export interface ActiveTextEditorVisibleContent { export interface VsCodeInlineController { selection: ActiveTextEditorSelection | null + selectionRange: ActiveTextEditorSelectionRange | null error(): Promise } @@ -92,6 +102,10 @@ export interface Editor< * Gets the active text editor's selection, or the entire file if the selected range is empty. */ getActiveTextEditorSelectionOrEntireFile(): ActiveTextEditorSelection | null + /** + * Get diagnostics (errors, warnings, hints) for a range within the active text editor. + */ + getActiveTextEditorDiagnosticsForRange(range: ActiveTextEditorSelectionRange): ActiveTextEditorDiagnostic[] | null getActiveTextEditorVisibleContent(): ActiveTextEditorVisibleContent | null replaceSelection(fileName: string, selectedText: string, replacement: string): Promise @@ -129,6 +143,10 @@ export class NoopEditor implements Editor { return null } + public getActiveTextEditorDiagnosticsForRange(): ActiveTextEditorDiagnostic[] | null { + return null + } + public getActiveTextEditorVisibleContent(): ActiveTextEditorVisibleContent | null { return null } diff --git a/lib/shared/src/intent-detector/client.ts b/lib/shared/src/intent-detector/client.ts index 1343a7c2ffc2..3543b669c999 100644 --- a/lib/shared/src/intent-detector/client.ts +++ b/lib/shared/src/intent-detector/client.ts @@ -1,11 +1,17 @@ +import { ANSWER_TOKENS } from '../prompt/constants' +import { Message } from '../sourcegraph-api' +import { SourcegraphCompletionsClient } from '../sourcegraph-api/completions/client' import { SourcegraphGraphQLAPIClient } from '../sourcegraph-api/graphql' -import { IntentDetector } from '.' +import { IntentClassificationOption, IntentDetector } from '.' const editorRegexps = [/editor/, /(open|current|this)\s+file/, /current(ly)?\s+open/, /have\s+open/] export class SourcegraphIntentDetectorClient implements IntentDetector { - constructor(private client: SourcegraphGraphQLAPIClient) {} + constructor( + private client: SourcegraphGraphQLAPIClient, + private completionsClient?: SourcegraphCompletionsClient + ) {} public isCodebaseContextRequired(input: string): Promise { return this.client.isContextRequiredForQuery(input) @@ -22,4 +28,108 @@ export class SourcegraphIntentDetectorClient implements IntentDetector { } return false } + + private buildInitialTranscript(options: IntentClassificationOption[]): Message[] { + const functions = options + .map(({ id, description }) => `Function ID: ${id}\nFunction Description: ${description}`) + .join('\n') + + return [ + { + speaker: 'human', + text: prompt.replace('{functions}', functions), + }, + { + speaker: 'assistant', + text: 'Ok.', + }, + ] + } + + private buildExampleTranscript(options: IntentClassificationOption[]): Message[] { + const messages = options.flatMap(({ id, examplePrompts }) => + examplePrompts.flatMap( + example => + [ + { + speaker: 'human', + text: example, + }, + { + speaker: 'assistant', + text: `${id}`, + }, + ] as const + ) + ) + + return messages + } + + public async classifyIntentFromOptions( + input: string, + options: IntentClassificationOption[], + fallback: Intent + ): Promise { + const completionsClient = this.completionsClient + if (!completionsClient) { + return fallback + } + + const preamble = this.buildInitialTranscript(options) + const examples = this.buildExampleTranscript(options) + + const result = await new Promise(resolve => { + let responseText = '' + return completionsClient.stream( + { + fast: true, + temperature: 0, + maxTokensToSample: ANSWER_TOKENS, + topK: -1, + topP: -1, + messages: [ + ...preamble, + ...examples, + { + speaker: 'human', + text: input, + }, + { + speaker: 'assistant', + }, + ], + }, + { + onChange: (text: string) => { + responseText = text + }, + onComplete: () => { + resolve(responseText) + }, + onError: (message: string, statusCode?: number) => { + console.error(`Error detecting intent: Status code ${statusCode}: ${message}`) + resolve(fallback) + }, + } + ) + }) + + const responseClassification = result.match(/(.*?)<\/classification>/)?.[1] + if (!responseClassification) { + return fallback + } + + return options.find(option => option.id === responseClassification)?.id ?? fallback + } } + +const prompt = ` +You are an AI assistant in a text editor. You are at expert at understanding the request of a software developer and selecting an available function to perform that request. +Think step-by-step to understand the request. +Only provide your response if you know the answer or can make a well-informed guess, otherwise respond with "unknown". +Enclose your response in XML tags. Do not provide anything else. + +Available functions: +{functions} +` diff --git a/lib/shared/src/intent-detector/index.ts b/lib/shared/src/intent-detector/index.ts index a8f5af296fdb..539afb641a65 100644 --- a/lib/shared/src/intent-detector/index.ts +++ b/lib/shared/src/intent-detector/index.ts @@ -1,4 +1,27 @@ +export interface IntentClassificationOption { + /** + * An identifier for this intent. + * This is what will be returned by the classifier. + */ + id: Intent + /** + * A description for this intent. + * Be specific in order to help the LLM understand the intent. + */ + description: string + /** + * Example prompts that match this intent. + * E.g. for a documentation intent: "Add documentation for this function" + */ + examplePrompts: string[] +} + export interface IntentDetector { isCodebaseContextRequired(input: string): Promise isEditorContextRequired(input: string): boolean | Error + classifyIntentFromOptions( + input: string, + options: IntentClassificationOption[], + fallback: Intent + ): Promise } diff --git a/lib/shared/src/prompt/templates.ts b/lib/shared/src/prompt/templates.ts index 348b0eea1faa..621c16633417 100644 --- a/lib/shared/src/prompt/templates.ts +++ b/lib/shared/src/prompt/templates.ts @@ -1,5 +1,7 @@ import path from 'path' +import { ActiveTextEditorDiagnostic } from '../editor' + const CODE_CONTEXT_TEMPLATE = `Use following code snippet from file \`{filePath}\`: \`\`\`{language} {text} @@ -66,6 +68,26 @@ export function populateCurrentEditorSelectedContextTemplate( ) } +const DIAGNOSTICS_CONTEXT_TEMPLATE = `Use the following {type} from the code snippet in the file \`{filePath}\` +{prefix}: {message} +Code snippet: +\`\`\`{language} +{code} +\`\`\`` + +export function populateCurrentEditorDiagnosticsTemplate( + { message, type, text }: ActiveTextEditorDiagnostic, + filePath: string +): string { + const language = getExtension(filePath) + return DIAGNOSTICS_CONTEXT_TEMPLATE.replace('{type}', type) + .replace('{filePath}', filePath) + .replace('{prefix}', type) + .replace('{message}', message) + .replace('{language}', language) + .replace('{code}', text) +} + const COMMAND_OUTPUT_TEMPLATE = 'Here is the output returned from the terminal.\n' export function populateTerminalOutputContextTemplate(output: string): string { diff --git a/lib/shared/src/test/mocks.ts b/lib/shared/src/test/mocks.ts index 078c03e07d08..a0656350baf7 100644 --- a/lib/shared/src/test/mocks.ts +++ b/lib/shared/src/test/mocks.ts @@ -3,9 +3,16 @@ import { URI } from 'vscode-uri' import { BotResponseMultiplexer } from '../chat/bot-response-multiplexer' import { RecipeContext } from '../chat/recipes/recipe' import { CodebaseContext } from '../codebase-context' -import { ActiveTextEditor, ActiveTextEditorSelection, ActiveTextEditorVisibleContent, Editor } from '../editor' +import { + ActiveTextEditor, + ActiveTextEditorDiagnostic, + ActiveTextEditorSelection, + ActiveTextEditorSelectionRange, + ActiveTextEditorVisibleContent, + Editor, +} from '../editor' import { EmbeddingsSearch } from '../embeddings' -import { IntentDetector } from '../intent-detector' +import { IntentClassificationOption, IntentDetector } from '../intent-detector' import { ContextResult, KeywordContextFetcher } from '../local-context' import { SourcegraphCompletionsClient } from '../sourcegraph-api/completions/client' import { CompletionParameters, CompletionResponse } from '../sourcegraph-api/completions/types' @@ -58,6 +65,14 @@ export class MockIntentDetector implements IntentDetector { public isEditorContextRequired(input: string): boolean | Error { return this.mocks.isEditorContextRequired?.(input) ?? false } + + public classifyIntentFromOptions( + input: string, + options: IntentClassificationOption[], + fallback: Intent + ): Promise { + return Promise.resolve(fallback) + } } export class MockKeywordContextFetcher implements KeywordContextFetcher { @@ -93,6 +108,12 @@ export class MockEditor implements Editor { return this.mocks.getActiveTextEditorSelection?.() ?? null } + public getActiveTextEditorDiagnosticsForRange( + range: ActiveTextEditorSelectionRange + ): ActiveTextEditorDiagnostic[] | null { + return this.mocks.getActiveTextEditorDiagnosticsForRange?.(range) ?? null + } + public getActiveTextEditor(): ActiveTextEditor | null { return this.mocks.getActiveTextEditor?.() ?? null } diff --git a/vscode/CHANGELOG.md b/vscode/CHANGELOG.md index 7a4ead7fbb7e..432f8808f7cd 100644 --- a/vscode/CHANGELOG.md +++ b/vscode/CHANGELOG.md @@ -8,12 +8,15 @@ Starting from `0.2.0`, Cody is using `major.EVEN_NUMBER.patch` for release versi ### Added +- Inline Fixups: Cody is now aware of errors, warnings and hints within your editor selection. [pull/376](https://github.com/sourcegraph/cody/pull/376) + ### Fixed - Bug: Chat History command shows chat view instead of history view. [pull/414](https://github.com/sourcegraph/cody/pull/414) ### Changed +- Inline Fixups: Added intent detection to improve prompt and context quality. [pull/376](https://github.com/sourcegraph/cody/pull/376) - Layout cleanups: smaller header and single line message input. [pull/449](https://github.com/sourcegraph/cody/pull/449) - Improve response feedback button behavior. [pull/451](https://github.com/sourcegraph/cody/pull/451) - Remove in-chat onboarding buttons for new chats. [pull/450](https://github.com/sourcegraph/cody/pull/450) diff --git a/vscode/src/chat/InlineChatViewProvider.ts b/vscode/src/chat/InlineChatViewProvider.ts index ca97fd2ca5ac..c36cea294023 100644 --- a/vscode/src/chat/InlineChatViewProvider.ts +++ b/vscode/src/chat/InlineChatViewProvider.ts @@ -51,6 +51,11 @@ export class InlineChatViewProvider extends MessageProvider { // We need to update the comment controller to support more than one active thread at a time. void vscode.commands.executeCommand('setContext', 'cody.inline.reply.pending', true) + /** + * TODO(umpox): + * We create a new comment and trigger the inline chat recipe, but may end up closing this comment and running a fix instead + * We should detect intent here (through regex and then `classifyIntentFromOptions`) and run the correct recipe/controller instead. + */ await this.editor.controllers.inline?.chat(reply, this.thread, isFixMode) this.editor.controllers.inline?.setResponsePending(true) await this.executeRecipe('inline-chat', reply.trimStart()) diff --git a/vscode/src/editor/vscode-editor.ts b/vscode/src/editor/vscode-editor.ts index bf66b9e041ab..220c7b054bca 100644 --- a/vscode/src/editor/vscode-editor.ts +++ b/vscode/src/editor/vscode-editor.ts @@ -2,7 +2,10 @@ import * as vscode from 'vscode' import type { ActiveTextEditor, + ActiveTextEditorDiagnostic, + ActiveTextEditorDiagnosticType, ActiveTextEditorSelection, + ActiveTextEditorSelectionRange, ActiveTextEditorViewControllers, ActiveTextEditorVisibleContent, Editor, @@ -91,6 +94,43 @@ export class VSCodeEditor implements Editor selectionRange.contains(diagnostic.range)) + .map(({ message, range, severity }) => ({ + type: this.getActiveTextEditorDiagnosticType(severity), + range, + text: activeEditor.document.getText(range), + message, + })) + } + private createActiveTextEditorSelection( activeEditor: vscode.TextEditor, selection: vscode.Selection diff --git a/vscode/src/external-services.ts b/vscode/src/external-services.ts index d36d339da34b..b0b111fba15f 100644 --- a/vscode/src/external-services.ts +++ b/vscode/src/external-services.ts @@ -70,7 +70,7 @@ export async function configureExternalServices( const guardrails = new SourcegraphGuardrailsClient(client) return { - intentDetector: new SourcegraphIntentDetectorClient(client), + intentDetector: new SourcegraphIntentDetectorClient(client, completions), codebaseContext, chatClient, completionsClient: completions, diff --git a/vscode/test/fixtures/mock-server.ts b/vscode/test/fixtures/mock-server.ts index 766fbd615854..db2d65afa55d 100644 --- a/vscode/test/fixtures/mock-server.ts +++ b/vscode/test/fixtures/mock-server.ts @@ -22,7 +22,8 @@ const responses = { fixup: 'Goodbye Cody', } -const FIXUP_PROMPT_TAG = '' +const FIXUP_PROMPT_TAG = '' +const NON_STOP_FIXUP_PROMPT_TAG = '' // Runs a stub Cody service for testing. export async function run(around: () => Promise): Promise { @@ -36,9 +37,11 @@ export async function run(around: () => Promise): Promise { // or have a method on the server to send a set response the next time it sees a trigger word in the request. const request = req as MockRequest const lastHumanMessageIndex = request.body.messages.length - 2 - const response = request.body.messages[lastHumanMessageIndex].text.includes(FIXUP_PROMPT_TAG) - ? responses.fixup - : responses.chat + const response = + request.body.messages[lastHumanMessageIndex].text.includes(FIXUP_PROMPT_TAG) || + request.body.messages[lastHumanMessageIndex].text.includes(NON_STOP_FIXUP_PROMPT_TAG) + ? responses.fixup + : responses.chat res.send(`event: completion\ndata: {"completion": ${JSON.stringify(response)}}\n\nevent: done\ndata: {}\n\n`) }) diff --git a/vscode/webviews/Recipes.tsx b/vscode/webviews/Recipes.tsx index bdc085b9ac4a..4430597b5745 100644 --- a/vscode/webviews/Recipes.tsx +++ b/vscode/webviews/Recipes.tsx @@ -9,7 +9,12 @@ import { VSCodeWrapper } from './utils/VSCodeApi' import styles from './Recipes.module.css' -type RecipeListType = Record +type ClickableRecipeID = Exclude< + RecipeID, + 'chat-question' | 'inline-touch' | 'inline-chat' | 'my-prompt' | 'next-questions' | 'non-stop' +> + +type RecipeListType = Record interface State { reorderedRecipes: RecipeListType @@ -61,7 +66,7 @@ export const Recipes: React.FunctionComponent<{ const reorderedRecipes: RecipeListType = {} as RecipeListType for (const recipe of newRecipes) { - reorderedRecipes[recipe[0]] = recipe[1] + reorderedRecipes[recipe[0] as ClickableRecipeID] = recipe[1] } setRecipes(reorderedRecipes) @@ -149,7 +154,7 @@ export const Recipes: React.FunctionComponent<{ index === draggedIndex && styles.recipeButtonDrag )} type="button" - onClick={() => onRecipeClick(key as RecipeID)} + onClick={() => onRecipeClick(key as ClickableRecipeID)} draggable={true} onDragStart={e => handleDragStart(e, index)} onDragOver={e => handleDragOver(e, index)} diff --git a/web/src/App.tsx b/web/src/App.tsx index 53a16c6d84ee..669b1088e3cf 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -23,6 +23,9 @@ const editor: Editor = { getActiveTextEditorSelectionOrEntireFile() { return null }, + getActiveTextEditorDiagnosticsForRange() { + return null + }, getActiveTextEditorVisibleContent() { return null },