Skip to content

Commit

Permalink
[Backport vscode-v1.46.x] Fetch standard prompts from remote prompts …
Browse files Browse the repository at this point in the history
…API (#6166)

Part of
[SRCH-1181](https://linear.app/sourcegraph/issue/SRCH-1181/pre-load-single-tenant-instances-to-have-ootb-prompts)

This PR checks the SG version and tries to fetch built-in prompts from
the prompt library rather than using local hardcoded commands. Later
prompts query will be unified and all custom/built-in prompts will be
fetched with one query

## Test plan
- Check after
https://linear.app/sourcegraph/issue/SRCH-1316/create-seeder-for-ootm-prompts
is done that the VSCode extension renders built-in prompts in the
welcome area and prompts tabs
 <br> Backport 224ef27 from #6150

Co-authored-by: Vova Kulikov <vova@sourcegraph.com>
  • Loading branch information
sourcegraph-release-bot and vovakulikov authored Nov 21, 2024
1 parent 7b37307 commit 45493e9
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 41 deletions.
9 changes: 8 additions & 1 deletion lib/shared/src/lexicalEditor/editorState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,9 @@ function lexicalEditorStateFromPromptString(
const [displayPath, maybeRange] = word.slice(1).split(':', 2)
const range = maybeRange ? parseRangeString(maybeRange) : undefined
const uri = refsByDisplayPath.get(displayPath)
const originalContextItem = opts?.additionalContextItemsMap?.get(word.slice(1))
const originalContextItem = opts?.additionalContextItemsMap?.get(
cleanTrailingSymbols(word.slice(1))
)

// Save previous last text or mention node before adding new mention
if ((originalContextItem || uri) && lastTextNode) {
Expand All @@ -329,6 +331,7 @@ function lexicalEditorStateFromPromptString(

if (originalContextItem) {
const contextItem = serializeContextItem(originalContextItem)
contextItem.range = range

children.push({
contextItem,
Expand Down Expand Up @@ -506,3 +509,7 @@ function textNode(text: string): SerializedTextNode {
text,
}
}

function cleanTrailingSymbols(str: string): string {
return str.replace(/[.,;:]+$/, '')
}
30 changes: 28 additions & 2 deletions lib/shared/src/sourcegraph-api/graphql/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { addCodyClientIdentificationHeaders } from '../client-name-version'
import { DOTCOM_URL, isDotCom } from '../environments'
import { isAbortError } from '../errors'
import {
BUILTIN_PROMPTS_QUERY,
CHANGE_PROMPT_VISIBILITY,
CHAT_INTENT_QUERY,
CONTEXT_FILTERS_QUERY,
Expand Down Expand Up @@ -450,7 +451,7 @@ export interface Prompt {
name: string
nameWithOwner: string
recommended: boolean
owner: {
owner?: {
namespaceName: string
}
description?: string
Expand All @@ -461,7 +462,7 @@ export interface Prompt {
text: string
}
url: string
createdBy: {
createdBy?: {
id: string
username: string
displayName: string
Expand Down Expand Up @@ -1269,6 +1270,31 @@ export class SourcegraphGraphQLAPIClient {
return result
}

public async queryBuiltinPrompts({
query,
first,
signal,
}: {
query: string
first?: number
signal?: AbortSignal
}): Promise<Prompt[]> {
const response = await this.fetchSourcegraphAPI<APIResponse<{ prompts: { nodes: Prompt[] } }>>(
BUILTIN_PROMPTS_QUERY,
{
query,
first: first ?? 100,
orderByMultiple: [PromptsOrderBy.PROMPT_UPDATED_AT],
},
signal
)
const result = extractDataOrError(response, data => data.prompts.nodes)
if (result instanceof Error) {
throw result
}
return result
}

public async createPrompt(input: PromptInput): Promise<{ id: string }> {
const response = await this.fetchSourcegraphAPI<APIResponse<{ createPrompt: { id: string } }>>(
CREATE_PROMPT_MUTATION,
Expand Down
30 changes: 30 additions & 0 deletions lib/shared/src/sourcegraph-api/graphql/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,36 @@ query ViewerPrompts($query: String, $first: Int!, $recommendedOnly: Boolean!, $o
}
}`

export const BUILTIN_PROMPTS_QUERY = `
query ViewerBuiltinPrompts($query: String!, $first: Int!, $orderByMultiple: [PromptsOrderBy!]) {
prompts(query: $query, first: $first, includeDrafts: false, recommendedOnly: false, builtinOnly: true, includeViewerDrafts: true, viewerIsAffiliated: true, orderByMultiple: $orderByMultiple) {
nodes {
id
name
nameWithOwner
recommended
owner {
namespaceName
}
description
draft
autoSubmit
mode
definition {
text
}
url
createdBy {
id
username
displayName
avatarURL
}
}
totalCount
}
}`

export const REPO_NAME_QUERY = `
query ResolveRepoName($cloneURL: String!) {
repository(cloneURL: $cloneURL) {
Expand Down
15 changes: 14 additions & 1 deletion vscode/src/chat/chat-view/ChatController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -951,7 +951,9 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
configuration: {
instruction,
mode,
intent: mode === 'edit' ? 'edit' : 'add',
// Only document code uses non-edit (insert mode), set doc intent for Document code prompt
// to specialize cody command runner for document code case.
intent: mode === 'edit' ? 'edit' : 'doc',
},
})

Expand All @@ -963,6 +965,17 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
const task = result.task

let responseMessage = `Here is the response for the ${task.intent} instruction:\n`

if (!task.diff && task.replacement) {
task.diff = [
{
type: 'insertion',
text: task.replacement,
range: task.originalRange,
},
]
}

task.diff?.map(diff => {
responseMessage += '\n```diff\n'
if (diff.type === 'deletion') {
Expand Down
27 changes: 26 additions & 1 deletion vscode/src/prompts/prompt-hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { getCurrentRepositoryInfo } from './utils'

export const PROMPT_CURRENT_FILE_PLACEHOLDER: string = 'cody://current-file'
export const PROMPT_CURRENT_SELECTION_PLACEHOLDER: string = 'cody://selection'
export const PROMPT_CURRENT_SELECTION_OLD_PLACEHOLDER: string = 'cody://current-selection'
export const PROMPT_CURRENT_DIRECTORY_PLACEHOLDER: string = 'cody://current-dir'
export const PROMPT_EDITOR_OPEN_TABS_PLACEHOLDER: string = 'cody://tabs'
export const PROMPT_CURRENT_REPOSITORY_PLACEHOLDER: string = 'cody://repository'
Expand All @@ -46,6 +47,7 @@ type PromptHydrationModifier = (
const PROMPT_HYDRATION_MODIFIERS: Record<string, PromptHydrationModifier> = {
[PROMPT_CURRENT_FILE_PLACEHOLDER]: hydrateWithCurrentFile,
[PROMPT_CURRENT_SELECTION_PLACEHOLDER]: hydrateWithCurrentSelection,
[PROMPT_CURRENT_SELECTION_OLD_PLACEHOLDER]: hydrateWithCurrentSelectionLegacy,
[PROMPT_CURRENT_DIRECTORY_PLACEHOLDER]: hydrateWithCurrentDirectory,
[PROMPT_EDITOR_OPEN_TABS_PLACEHOLDER]: hydrateWithOpenTabs,
[PROMPT_CURRENT_REPOSITORY_PLACEHOLDER]: hydrateWithCurrentWorkspace,
Expand All @@ -62,7 +64,7 @@ export async function hydratePromptText(
const promptText = PromptString.unsafe_fromUserQuery(promptRawText)

// Match any general cody mentions in the prompt text with cody:// prefix
const promptTextMentionMatches = promptText.toString().match(/cody:\/\/\S+/gm) ?? []
const promptTextMentionMatches = promptText.toString().match(/cody:\/\/[^\s.,;:]+/gm) ?? []

let hydratedPromptText = promptText
const contextItemsMap = new Map<string, ContextItem>()
Expand Down Expand Up @@ -132,6 +134,29 @@ async function hydrateWithCurrentSelection(
]
}

async function hydrateWithCurrentSelectionLegacy(
promptText: PromptString,
initialContext: PromptHydrationInitialContext
): Promise<[PromptString, ContextItem[]]> {
// Check if initial context already contains current file with selection (Cody Web case)
const initialContextFile = initialContext.find(item => item.type === 'file' && item.range)

const currentSelection = initialContextFile ?? (await getSelectionOrFileContext())[0]

// TODO (vk): Add support for error notification if prompt hydration fails
if (!currentSelection) {
return [promptText, []]
}

return [
promptText.replaceAll(
PROMPT_CURRENT_SELECTION_OLD_PLACEHOLDER,
selectedCodePromptWithExtraFiles(currentSelection, [])
),
[currentSelection],
]
}

async function hydrateWithCurrentDirectory(
promptText: PromptString,
initialContext: PromptHydrationInitialContext
Expand Down
71 changes: 51 additions & 20 deletions vscode/src/prompts/prompts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
type Action,
type CommandAction,
FeatureFlag,
type PromptAction,
Expand All @@ -9,6 +10,7 @@ import {
graphqlClient,
isAbortError,
isErrorLike,
isValidVersion,
} from '@sourcegraph/cody-shared'
import { FIXTURE_COMMANDS } from '../../webviews/components/promptList/fixtures'
import { getCodyCommandList } from '../commands/CommandsController'
Expand Down Expand Up @@ -75,30 +77,22 @@ export async function mergedPromptsAndLegacyCommands(
): Promise<PromptsResult> {
const { query, recommendedOnly, first } = input
const queryLower = query.toLowerCase()
const [customPrompts, isUnifiedPromptsEnabled] = await Promise.all([
const [customPrompts, isUnifiedPromptsEnabled, isNewPromptsSgVersion] = await Promise.all([
fetchCustomPrompts(queryLower, first, recommendedOnly, signal),

// Unified prompts flag provides prompts-like commands API
featureFlagProvider.evaluateFeatureFlagEphemerally(FeatureFlag.CodyUnifiedPrompts),
])

const codyCommands = getCodyCommandList()
const allCommands: CommandAction[] = !clientCapabilities().isCodyWeb
? // Ignore commands since with unified prompts vital commands will be replaced by out-of-box
// prompts, see main.ts register cody commands for unified prompts
isUnifiedPromptsEnabled
? STANDARD_PROMPTS_LIKE_COMMAND
: [...codyCommands, ...(USE_CUSTOM_COMMANDS_FIXTURE ? FIXTURE_COMMANDS : [])].map(c => ({
...c,
actionType: 'command',
}))
: // Ignore any commands for Cody Web since no commands are supported
[]
// 5.10.0 Contains new prompts library API which provides unified prompts
// and standard (built-in) prompts
isValidVersion({ minimumVersion: '5.10.0' }),
])

const matchingCommands = allCommands.filter(
c =>
matchesQuery(queryLower, c.key) ||
matchesQuery(queryLower, c.description ?? '') ||
matchesQuery(queryLower, c.prompt)
)
const matchingCommands = await getLocalCommands({
query: queryLower,
isUnifiedPromptsEnabled,
remoteBuiltinPrompts: isNewPromptsSgVersion,
})

const actions =
customPrompts === 'unsupported' ? matchingCommands : [...customPrompts, ...matchingCommands]
Expand Down Expand Up @@ -141,3 +135,40 @@ async function fetchCustomPrompts(
return []
}
}

interface LocalCommandsInput {
query: string
isUnifiedPromptsEnabled: boolean
remoteBuiltinPrompts: boolean
}

async function getLocalCommands(input: LocalCommandsInput): Promise<Action[]> {
const { query, isUnifiedPromptsEnabled, remoteBuiltinPrompts } = input

// Fetch standards (built-in) prompts from prompts library API
if (remoteBuiltinPrompts) {
const remoteStandardPrompts = await graphqlClient.queryBuiltinPrompts({ query })
return remoteStandardPrompts.map(prompt => ({ ...prompt, actionType: 'prompt' }))
}

// Fallback on local commands (prompts-like or not is controlled by CodyUnifiedPrompts feature flag)
const codyCommands = getCodyCommandList()
const allCommands: CommandAction[] = !clientCapabilities().isCodyWeb
? // Ignore commands since with unified prompts vital commands will be replaced by out-of-box
// prompts, see main.ts register cody commands for unified prompts
isUnifiedPromptsEnabled
? STANDARD_PROMPTS_LIKE_COMMAND
: [...codyCommands, ...(USE_CUSTOM_COMMANDS_FIXTURE ? FIXTURE_COMMANDS : [])].map(c => ({
...c,
actionType: 'command',
}))
: // Ignore any commands for Cody Web since no commands are supported
[]

return allCommands.filter(
c =>
matchesQuery(query, c.key) ||
matchesQuery(query, c.description ?? '') ||
matchesQuery(query, c.prompt)
)
}
18 changes: 15 additions & 3 deletions vscode/test/e2e/command-core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ test.extend<ExpectedV2Events>({
'cody.fixup.response:hasCode',
'cody.fixup.apply:succeeded',
],
})('Generate Unit Test Command (Edit)', async ({ page, sidebar }) => {
})('Generate Unit Test Command (Edit)', async ({ page, sidebar, server }) => {
server.onGraphQl('SiteProductVersion').replyJson({
data: { site: { productVersion: '5.9.0' } },
})

// Sign into Cody
await sidebarSignin(page, sidebar)

Expand Down Expand Up @@ -61,7 +65,11 @@ test.extend<ExpectedV2Events>({
'cody.fixup.response:hasCode',
'cody.fixup.apply:succeeded',
],
})('Document Command (Edit)', async ({ page, sidebar }) => {
})('Document Command (Edit)', async ({ page, sidebar, server }) => {
server.onGraphQl('SiteProductVersion').replyJson({
data: { site: { productVersion: '5.9.0' } },
})

// Sign into Cody
await sidebarSignin(page, sidebar)

Expand Down Expand Up @@ -103,7 +111,11 @@ test.extend<ExpectedV2Events>({
'cody.auth:connected',
'cody.command.explain:executed',
],
})('Explain Command from Prompts Tab', async ({ page, sidebar }) => {
})('Explain Command from Prompts Tab', async ({ page, sidebar, server }) => {
server.onGraphQl('SiteProductVersion').replyJson({
data: { site: { productVersion: '5.9.0' } },
})

// Sign into Cody
await sidebarSignin(page, sidebar)

Expand Down
18 changes: 13 additions & 5 deletions vscode/webviews/components/promptList/ActionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,19 @@ const ActionPrompt: FC<ActionPromptProps> = props => {

return (
<div className={styles.prompt}>
<UserAvatar
size={22}
user={{ ...prompt.createdBy, endpoint: '' }}
className={styles.promptAvatar}
/>
{prompt.createdBy && (
<UserAvatar
size={22}
user={{ ...prompt.createdBy, endpoint: '' }}
className={styles.promptAvatar}
/>
)}

{!prompt.createdBy && (
<div className={styles.promptAvatar}>
<PencilRuler size={16} strokeWidth={1.5} className={styles.promptIcon} />
</div>
)}

<div className={styles.promptContent}>
<div className={styles.promptTitle}>
Expand Down
Loading

0 comments on commit 45493e9

Please sign in to comment.