Skip to content
This repository was archived by the owner on Aug 1, 2025. It is now read-only.

Commit 801a122

Browse files
authored
custom prompts: add intro and new context type (#255)
This PR adds new context types to the custom prompts feature. Users can specify what type of context to include when building their recipe: ``` export interface CodyPromptContext { codebase: boolean openTabs?: boolean currentDir?: boolean excludeSelection?: boolean } ``` Add intro to My Prompts and button to create cody.json file in current workspace: ![image](https://github.com/sourcegraph/cody/assets/68532117/5f54f69a-675d-4d05-aa34-b2c715c97420)
1 parent 19c9aef commit 801a122

File tree

10 files changed

+256
-65
lines changed

10 files changed

+256
-65
lines changed

.vscode/cody.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"description": "This file is used for building custom workspace recipes for Cody by Sourcegraph.",
3+
"recipes": {
4+
"Spell Checker": {
5+
"prompt": "Spell check the selected code and let me know if I have any typos or non-standard usage.",
6+
"context": {
7+
"codebase": false
8+
}
9+
},
10+
"Refactor Code": {
11+
"prompt": "Suggest ways to refactor the selected code to improve readability and maintainability",
12+
"context": {
13+
"currentDir": true
14+
}
15+
},
16+
"Compare Files in Opened Tabs": {
17+
"prompt": "Compare the code from the shared files and explain how they are related",
18+
"context": {
19+
"openTabs": true,
20+
"excludeSelection": true
21+
}
22+
},
23+
"GH Search: Deploy Repoes not owned by Sourcegraph": {
24+
"prompt": "What are the names of repositories that are used for deploying Sourcegraph, but is not directly owned by Sourcegraph?",
25+
"command": "gh",
26+
"args": ["search", "repos", "sourcegraph", "deploy"],
27+
"note": "You must have gh command installed and authenticated to use this recipe"
28+
},
29+
"Last Git Commit Info": {
30+
"prompt": "Who made the last commit to this repository and what did they change?",
31+
"command": "git",
32+
"args": ["log", "-1"],
33+
"note": "You must have git command installed and authenticated to use this recipe"
34+
}
35+
}
36+
}

lib/shared/src/chat/recipes/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function getFileExtension(fileName: string): string {
6262
// ex. Remove `tags:` that Cody sometimes include in the returned content
6363
// It also removes all spaces before a new line to keep the indentations
6464
export function contentSanitizer(text: string): string {
65-
let output = text.trimEnd().replace(/<\/selection>$/, '')
65+
let output = text.replace(/<\/selection>\s$/, '')
6666
const tagsIndex = text.indexOf('tags:')
6767
if (tagsIndex !== -1) {
6868
// NOTE: 6 is the length of `tags:` + 1 space

lib/shared/src/chat/recipes/my-prompt.ts

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class MyPrompt implements Recipe {
2727
// Match human input with key from promptStore to get prompt text when there is none
2828
let promptText = humanInput || this.promptStore.get(humanInput) || null
2929
if (!promptText) {
30-
await vscode.window.showErrorMessage('Please enter a valid for the recipe.')
30+
await vscode.window.showErrorMessage('Please enter a valid prompt for the recipe.')
3131
return null
3232
}
3333

@@ -37,7 +37,7 @@ export class MyPrompt implements Recipe {
3737
const commandOutput = context.editor.controllers?.prompt.get()
3838
if (commandOutput) {
3939
// promptText += `\n${commandOutput}\n`
40-
promptText += 'Please refer to the command output when answering my quesiton.'
40+
promptText += 'Please refer to the command output or the code I am looking at to answer my quesiton.'
4141
}
4242

4343
// Add selection file name as display when available
@@ -67,67 +67,96 @@ export class MyPrompt implements Recipe {
6767
commandOutput?: string | null
6868
): Promise<ContextMessage[]> {
6969
const contextMessages: ContextMessage[] = []
70-
71-
const isCodebaseContextRequired = editor.controllers?.prompt.get('context')
72-
if (isCodebaseContextRequired) {
73-
await vscode.window.showInformationMessage('Codebase is not required.')
70+
const contextConfig = editor.controllers?.prompt.get('context')
71+
const isCodebaseContextRequired = contextConfig
72+
? (JSON.parse(contextConfig) as CodyPromptContext)
73+
: defaultCodyPromptContext
74+
// Codebase context is not included by default
75+
if (isCodebaseContextRequired.codebase) {
7476
const codebaseContextMessages = await codebaseContext.getContextMessages(text, {
7577
numCodeResults: 12,
7678
numTextResults: 3,
7779
})
7880
contextMessages.push(...codebaseContextMessages)
79-
80-
// Create context messages from open tabs
81-
if (contextMessages.length < 10) {
82-
contextMessages.push(...MyPrompt.getEditorOpenTabsContext())
83-
}
8481
}
85-
82+
// Create context messages from open tabs
83+
if (isCodebaseContextRequired.openTabs) {
84+
const openTabsContext = await MyPrompt.getEditorOpenTabsContext()
85+
contextMessages.push(...openTabsContext)
86+
}
87+
// Create context messages from current directory
88+
if (isCodebaseContextRequired.currentDir) {
89+
const currentDirContext = await MyPrompt.getCurrentDirContext()
90+
contextMessages.push(...currentDirContext)
91+
}
8692
// Add selected text as context when available
87-
if (selection?.selectedText) {
93+
if (selection?.selectedText && !isCodebaseContextRequired.excludeSelection) {
8894
contextMessages.push(...ChatQuestion.getEditorSelectionContext(selection))
8995
}
90-
9196
// Create context messages from terminal output if any
9297
if (commandOutput) {
9398
contextMessages.push(...MyPrompt.getTerminalOutputContext(commandOutput))
9499
}
95-
96100
return contextMessages.slice(-12)
97101
}
98102

99103
// Get context from current editor open tabs
100-
public static getEditorOpenTabsContext(): ContextMessage[] {
104+
public static async getEditorOpenTabsContext(): Promise<ContextMessage[]> {
101105
const contextMessages: ContextMessage[] = []
102-
// Skip the current active tab (which is already included in selection context), files in currentDir, and non-file tabs
103-
const openTabs = vscode.window.visibleTextEditors
104-
for (const tab of openTabs) {
105-
if (tab === vscode.window.activeTextEditor || tab.document.uri.scheme !== 'file') {
106+
// Get a list of the open tabs
107+
const openTabs = vscode.window.tabGroups.all
108+
const files = openTabs.flatMap(group => group.tabs.map(tab => tab.input)) as vscode.TabInputText[]
109+
for (const doc of files) {
110+
if (doc.uri.scheme !== 'file') {
106111
continue
107112
}
108-
const fileName = tab.document.fileName
109-
const truncatedContent = truncateText(tab.document.getText(), MAX_CURRENT_FILE_TOKENS)
110-
const contextMessage = getContextMessageWithResponse(
113+
const fileName = vscode.workspace.asRelativePath(doc.uri.fsPath)
114+
// remove workspace root path from fileName
115+
const fileContent = await vscode.workspace.openTextDocument(doc.uri)
116+
const truncatedContent = truncateText(fileContent.getText(), MAX_CURRENT_FILE_TOKENS)
117+
const docAsMessage = getContextMessageWithResponse(
111118
populateCurrentEditorContextTemplate(truncatedContent, fileName),
112-
{
113-
fileName,
114-
}
119+
{ fileName }
115120
)
116-
contextMessages.push(...contextMessage)
121+
contextMessages.push(...docAsMessage)
117122
}
118123
return contextMessages
119124
}
120125

121-
// Get display text for human
122-
private getHumanDisplayText(humanChatInput: string, fileName: string): string {
123-
return humanChatInput + InlineTouch.displayPrompt + fileName
124-
}
125-
126126
public static getTerminalOutputContext(output: string): ContextMessage[] {
127127
const truncatedContent = truncateText(output, MAX_CURRENT_FILE_TOKENS)
128128
return [
129129
{ speaker: 'human', text: populateTerminalOutputContextTemplate(truncatedContent) },
130130
{ speaker: 'assistant', text: 'OK.' },
131131
]
132132
}
133+
134+
// Create Context from Current Directory of Active Document
135+
public static async getCurrentDirContext(): Promise<ContextMessage[]> {
136+
// get current document file path
137+
const currentDoc = vscode.window.activeTextEditor?.document
138+
if (!currentDoc) {
139+
return []
140+
}
141+
return InlineTouch.getEditorDirContext(currentDoc.fileName.replace(/\/[^/]+$/, ''))
142+
}
143+
144+
// Get display text for human
145+
private getHumanDisplayText(humanChatInput: string, fileName: string): string {
146+
return humanChatInput + InlineTouch.displayPrompt + fileName
147+
}
148+
}
149+
150+
export interface CodyPromptContext {
151+
codebase: boolean
152+
openTabs?: boolean
153+
currentDir?: boolean
154+
excludeSelection?: boolean
155+
}
156+
157+
export const defaultCodyPromptContext: CodyPromptContext = {
158+
codebase: false,
159+
openTabs: false,
160+
currentDir: false,
161+
excludeSelection: false,
133162
}

vscode/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ Starting from `0.2.0`, Cody is using `major.EVEN_NUMBER.patch` for release versi
99
### Added
1010

1111
- Added support for the CMD+K hotkey to clear the code chat history. [pull/245](https://github.com/sourcegraph/cody/pull/245)
12+
- [Internal Only] `Custom Recipe` is available for S2 internal users for testing purpose. [pull/81](https://github.com/sourcegraph/cody/pull/81)
1213

1314
### Fixed
1415

1516
- Fixed a bug that caused messages to disappear when signed-in users encounter an authentication error. [pull/201](https://github.com/sourcegraph/cody/pull/201)
1617
- Inline Chat: Since last version, running Inline Fixups would add an additional `</selection>` tag to the end of the code edited by Cody, which has now been removed. [pull/182](https://github.com/sourcegraph/cody/pull/182)
1718
- Chat Command: Fixed an issue where /r(est) had a trailing space. [pull/245](https://github.com/sourcegraph/cody/pull/245)
19+
- Inline Fixups: Fixed a regression where Cody's inline fixup suggestions were not properly replacing the user's selection. [pull/70](https://github.com/sourcegraph/cody/pull/70)
1820

1921
### Changed
2022

vscode/resources/bin/cody.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"description": "This file is used for building custom workspace recipes for Cody by Sourcegraph.",
3+
"recipes": {
4+
"Spell Checker": {
5+
"prompt": "Spell check the selected code and let me know if I have any typos or non-standard usage.",
6+
"context": {
7+
"codebase": false
8+
}
9+
},
10+
"Refactor Code": {
11+
"prompt": "Suggest ways to refactor the selected code to improve readability and maintainability",
12+
"context": {
13+
"currentDir": true
14+
}
15+
},
16+
"Compare Files in Opened Tabs": {
17+
"prompt": "Compare the code from the shared files and explain how they are related",
18+
"context": {
19+
"openTabs": true,
20+
"excludeSelection": true
21+
}
22+
},
23+
"GH Search: Deploy Repoes not owned by Sourcegraph": {
24+
"prompt": "What are the names of repositories that are used for deploying Sourcegraph, but is not directly owned by Sourcegraph?",
25+
"command": "gh",
26+
"args": ["search", "repos", "sourcegraph", "deploy"],
27+
"note": "You must have gh command installed and authenticated to use this recipe"
28+
},
29+
"Last Git Commit Info": {
30+
"prompt": "Who made the last commit to this repository and what did they change?",
31+
"command": "git",
32+
"args": ["log", "-1"],
33+
"note": "You must have git command installed and authenticated to use this recipe"
34+
}
35+
}
36+
}

vscode/src/chat/ChatViewProvider.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,11 +717,42 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
717717
return
718718
}
719719
// Create a new recipe
720-
if (title === 'new') {
720+
if (title === 'add') {
721721
await this.editor.controllers.prompt.add()
722722
await this.sendMyPrompts()
723723
return
724724
}
725+
// Clear all recipes stored in user global storage
726+
if (title === 'clear') {
727+
await this.editor.controllers.prompt.clear()
728+
await this.sendMyPrompts()
729+
return
730+
}
731+
if (title === 'new-workspace-example-file') {
732+
if (!this.currentWorkspaceRoot) {
733+
void vscode.window.showErrorMessage('Could not find workspace path.')
734+
return
735+
}
736+
try {
737+
// copy the cody.json file from the extension path and move it to the workspace root directory
738+
// Find the fsPath of the cody.json example file from the this.rgPath
739+
const extensionPath = this.rgPath.slice(0, Math.max(0, this.rgPath.lastIndexOf('/')))
740+
const extensionUri = vscode.Uri.parse(extensionPath)
741+
const codyJsonPath = vscode.Uri.joinPath(extensionUri, 'cody.json')
742+
const bytes = await vscode.workspace.fs.readFile(codyJsonPath)
743+
const decoded = new TextDecoder('utf-8').decode(bytes)
744+
const workspaceUri = vscode.Uri.parse(this.currentWorkspaceRoot)
745+
const workspaceCodyJsonPath = vscode.Uri.joinPath(workspaceUri, '.vscode/cody.json')
746+
const workspaceEditor = new vscode.WorkspaceEdit()
747+
workspaceEditor.createFile(workspaceCodyJsonPath, { ignoreIfExists: false })
748+
workspaceEditor.insert(workspaceCodyJsonPath, new vscode.Position(0, 0), decoded)
749+
await vscode.workspace.applyEdit(workspaceEditor)
750+
await vscode.window.showTextDocument(workspaceCodyJsonPath)
751+
} catch (error) {
752+
void vscode.window.showErrorMessage(`Could not create a new cody.json file: ${error}`)
753+
}
754+
return
755+
}
725756
this.showTab('chat')
726757
// Get prompt details from controller by title then execute
727758
const prompt = this.editor.controllers.prompt.find(title)

vscode/src/my-cody/MyPromptController.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as vscode from 'vscode'
22

3+
import { CodyPromptContext, defaultCodyPromptContext } from '@sourcegraph/cody-shared/src/chat/recipes/my-prompt'
34
import { Message } from '@sourcegraph/cody-shared/src/sourcegraph-api'
45

56
import { isInternalUser } from '../chat/protocol'
@@ -15,10 +16,7 @@ interface CodyPrompt {
1516
prompt: string
1617
command?: string
1718
args?: string[]
18-
context?: {
19-
codebase: boolean
20-
openTabs?: boolean
21-
}
19+
context?: CodyPromptContext
2220
type?: CodyPromptType
2321
}
2422

@@ -73,6 +71,10 @@ export class MyPromptController {
7371
// getter for the promptInProgress
7472
public get(type?: string): string | null {
7573
if (type === 'context') {
74+
const contextConfig = this.myPromptInProgress?.context
75+
return JSON.stringify(contextConfig || defaultCodyPromptContext)
76+
}
77+
if (type === 'codebase') {
7678
return this.myPromptInProgress?.context?.codebase ? 'codebase' : null
7779
}
7880
// return the terminal output from the last command run
@@ -145,15 +147,27 @@ export class MyPromptController {
145147
this.myPromptStore = await this.builder.get()
146148
}
147149

150+
public async clear(): Promise<void> {
151+
if (!this.builder.userPromptsSize) {
152+
void vscode.window.showInformationMessage(
153+
'No User Recipes to remove. If you want to remove Workspace Recipes, please remove the .vscode/cody.json file from your repository.'
154+
)
155+
}
156+
await this.context.globalState.update(MY_CODY_PROMPTS_KEY, null)
157+
}
158+
148159
public async add(): Promise<void> {
149160
// Get the prompt name and prompt description from the user using the input box with 2 steps
150161
const promptName = await vscode.window.showInputBox({
151162
title: 'Creating a new custom recipe...',
152-
prompt: 'What is the name for the new recipe?',
163+
prompt: 'Enter an unique name for the new recipe.',
153164
placeHolder: 'e,g. Vulnerability Scanner',
154165
validateInput: (input: string) => {
155-
if (!input) {
156-
return 'Please enter a prompt name.'
166+
if (!input || input === 'add' || input === 'clear') {
167+
return 'Please enter a valid name for the recipe.'
168+
}
169+
if (this.myPromptStore.has(input)) {
170+
return 'A recipe with the same name already exists. Please enter a different name.'
157171
}
158172
return
159173
},
@@ -180,7 +194,7 @@ export class MyPromptController {
180194
const promptCommand = await vscode.window.showInputBox({
181195
title: 'Creating a new custom recipe...',
182196
prompt: '[Optional] Add a terminal command for the recipe to run from your current workspace. The output will be shared with Cody as context for the prompt. (The added command must work on your local machine.)',
183-
placeHolder: 'e,g. node your-script.js, git describe --long, etc.',
197+
placeHolder: 'e,g. node your-script.js, git describe --long, cat src/file-name.js etc.',
184198
})
185199
if (promptCommand) {
186200
const commandParts = promptCommand.split(' ')
@@ -201,6 +215,8 @@ class MyRecipesBuilder {
201215
public myPromptsMap = new Map<string, CodyPrompt>()
202216
public idSet = new Set<string>()
203217

218+
public userPromptsSize = 0
219+
204220
public codebase: string | null = null
205221

206222
constructor(private globalState: vscode.Memento, private workspaceRoot: string | null) {}
@@ -211,7 +227,8 @@ class MyRecipesBuilder {
211227
this.idSet = new Set<string>()
212228
// user prompts
213229
const storagePrompts = this.getPromptsFromExtensionStorage()
214-
this.build(storagePrompts, 'user')
230+
const storagePromptsMap = this.build(storagePrompts, 'user')
231+
this.userPromptsSize = storagePromptsMap?.size || 0
215232
// workspace prompts
216233
const wsPrompts = await this.getPromptsFromWorkspace(this.workspaceRoot)
217234
this.build(wsPrompts, 'workspace')

vscode/src/services/DecorationProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class DecorationProvider {
9090
*/
9191
public setState(status: CodyTaskState, newRange: vscode.Range): void {
9292
this.status = status
93-
this.range = new vscode.Range(newRange.start.line, 0, Math.max(0, newRange.end.line - 1), 0)
93+
this.range = new vscode.Range(newRange.start.line, 0, Math.max(0, newRange.end.line), 0)
9494
this.decorate()
9595
this._onDidChange.fire()
9696
}

vscode/webviews/Recipes.module.css

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616
justify-content: space-between;
1717
align-items: center;
1818

19-
font-size: 1rem;
19+
font-size: 0.8rem;
2020
font-weight: bold;
2121
}
22+
23+
.recipes-notes {
24+
display: flex;
25+
align-items: center;
26+
margin: 0.1rem 0;
27+
font-size: small;
28+
}

0 commit comments

Comments
 (0)