Skip to content

Commit

Permalink
"Prompt" chat toolbar item to add a prompt from Prompt Library (#4903)
Browse files Browse the repository at this point in the history
> NOTE: Only review the last commit (`Prompt selector in chat to select
a prompt from the Prompt Library`). The other commits all have their own
separate PRs opened.

The [Prompt Library](https://sourcegraph.com/prompts) lets you save and
share useful prompts for Cody. Now, you can insert a prompt from your
Prompt Library into your Cody chat message with this new **Prompts**
selector in the message editor toolbar.
    
Feature-flagged off by default (`chat-prompt-selector`).


![image](https://github.com/user-attachments/assets/39d88e1a-8f77-4adc-a30d-6871c0b8146f)

## Test plan

Enable the `chat-prompt-selector` feature flag for your user on dotcom.
Try adding the `sqs/gen-test-react-vitest` prompt. It should append the
prompt's text in your chat message editor.
  • Loading branch information
sqs authored Jul 27, 2024
1 parent f63d462 commit 921b68a
Show file tree
Hide file tree
Showing 25 changed files with 805 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ object Constants {
const val setConfigFeatures = "setConfigFeatures"
const val allMentionProvidersMetadata = "allMentionProvidersMetadata"
const val updateEditorState = "updateEditorState"
const val queryPrompts_response = "queryPrompts/response"
const val embeddings = "embeddings"
const val indeterminate = "indeterminate"
const val `no-match` = "no-match"
Expand Down Expand Up @@ -160,6 +161,7 @@ object Constants {
const val troubleshoot_reloadAuth = "troubleshoot/reloadAuth"
const val getAllMentionProvidersMetadata = "getAllMentionProvidersMetadata"
const val `experimental-unit-test-prompt` = "experimental-unit-test-prompt"
const val queryPrompts = "queryPrompts"
const val Automatic = "Automatic"
const val Invoke = "Invoke"
const val none = "none"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@file:Suppress("FunctionName", "ClassName", "unused", "EnumEntryName", "UnusedImport")
package com.sourcegraph.cody.agent.protocol_generated;

data class DefinitionParams(
val text: String,
)

Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ sealed class ExtensionMessage {
"setConfigFeatures" -> context.deserialize<SetConfigFeaturesExtensionMessage>(element, SetConfigFeaturesExtensionMessage::class.java)
"allMentionProvidersMetadata" -> context.deserialize<AllMentionProvidersMetadataExtensionMessage>(element, AllMentionProvidersMetadataExtensionMessage::class.java)
"updateEditorState" -> context.deserialize<UpdateEditorStateExtensionMessage>(element, UpdateEditorStateExtensionMessage::class.java)
"queryPrompts/response" -> context.deserialize<QueryPrompts_responseExtensionMessage>(element, QueryPrompts_responseExtensionMessage::class.java)
else -> throw Exception("Unknown discriminator ${element}")
}
}
Expand Down Expand Up @@ -197,6 +198,7 @@ data class `context_remote-reposExtensionMessage`(
data class SetConfigFeaturesExtensionMessage(
val type: TypeEnum, // Oneof: setConfigFeatures
val configFeatures: ConfigFeaturesParams,
val exportedFeatureFlags: Map<String, Boolean>,
) : ExtensionMessage() {

enum class TypeEnum {
Expand Down Expand Up @@ -224,3 +226,14 @@ data class UpdateEditorStateExtensionMessage(
}
}

data class QueryPrompts_responseExtensionMessage(
val type: TypeEnum, // Oneof: queryPrompts/response
val result: List<Prompt>? = null,
val error: String? = null,
) : ExtensionMessage() {

enum class TypeEnum {
@SerializedName("queryPrompts/response") QueryPrompts_response,
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@file:Suppress("FunctionName", "ClassName", "unused", "EnumEntryName", "UnusedImport")
package com.sourcegraph.cody.agent.protocol_generated;

data class OwnerParams(
val namespaceName: String,
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@file:Suppress("FunctionName", "ClassName", "unused", "EnumEntryName", "UnusedImport")
package com.sourcegraph.cody.agent.protocol_generated;

data class Prompt(
val id: String,
val name: String,
val nameWithOwner: String,
val owner: OwnerParams,
val description: String? = null,
val draft: Boolean,
val definition: DefinitionParams,
val url: String,
)

Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ sealed class WebviewMessage {
"troubleshoot/reloadAuth" -> context.deserialize<Troubleshoot_reloadAuthWebviewMessage>(element, Troubleshoot_reloadAuthWebviewMessage::class.java)
"getAllMentionProvidersMetadata" -> context.deserialize<GetAllMentionProvidersMetadataWebviewMessage>(element, GetAllMentionProvidersMetadataWebviewMessage::class.java)
"experimental-unit-test-prompt" -> context.deserialize<`experimental-unit-test-promptWebviewMessage`>(element, `experimental-unit-test-promptWebviewMessage`::class.java)
"queryPrompts" -> context.deserialize<QueryPromptsWebviewMessage>(element, QueryPromptsWebviewMessage::class.java)
else -> throw Exception("Unknown discriminator ${element}")
}
}
Expand Down Expand Up @@ -434,3 +435,13 @@ data class `experimental-unit-test-promptWebviewMessage`(
}
}

data class QueryPromptsWebviewMessage(
val command: CommandEnum, // Oneof: queryPrompts
val query: String,
) : WebviewMessage() {

enum class CommandEnum {
@SerializedName("queryPrompts") QueryPrompts,
}
}

2 changes: 2 additions & 0 deletions lib/shared/src/experimentation/FeatureFlagProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export enum FeatureFlag {

/** Chat in sidebar */
CodyChatInSidebar = 'cody-chat-in-sidebar',

ChatPromptSelector = 'chat-prompt-selector',
}

const ONE_HOUR = 60 * 60 * 1000
Expand Down
1 change: 1 addition & 0 deletions lib/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export type {
CodyLLMSiteConfiguration,
ContextSearchResult,
EmbeddingsSearchResult,
Prompt,
event,
} from './sourcegraph-api/graphql/client'
export { RestClient } from './sourcegraph-api/rest/client'
Expand Down
31 changes: 31 additions & 0 deletions lib/shared/src/sourcegraph-api/graphql/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
LOG_EVENT_MUTATION,
LOG_EVENT_MUTATION_DEPRECATED,
PACKAGE_LIST_QUERY,
PROMPTS_QUERY,
RANK_CONTEXT_QUERY,
RECORD_CONTEXT_QUERY,
RECORD_TELEMETRY_EVENTS_MUTATION,
Expand Down Expand Up @@ -388,6 +389,24 @@ export interface ContextSearchResult {
content: string
}

/**
* A prompt that can be shared and reused. See Prompt in the Sourcegraph GraphQL API.
*/
export interface Prompt {
id: string
name: string
nameWithOwner: string
owner: {
namespaceName: string
}
description?: string
draft: boolean
definition: {
text: string
}
url: string
}

interface ContextFiltersResponse {
site: {
codyContextFilters: {
Expand Down Expand Up @@ -997,6 +1016,18 @@ export class SourcegraphGraphQLAPIClient {
return result
}

public async queryPrompts(query: string): Promise<Prompt[]> {
const response = await this.fetchSourcegraphAPI<APIResponse<{ prompts: { nodes: Prompt[] } }>>(
PROMPTS_QUERY,
{ query }
)
const result = extractDataOrError(response, data => data.prompts.nodes)
if (result instanceof Error) {
throw result
}
return result
}

/**
* Checks if Cody is enabled on the current Sourcegraph instance.
* @returns
Expand Down
29 changes: 27 additions & 2 deletions lib/shared/src/sourcegraph-api/graphql/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,31 @@ query ContextFilters {
}
}`

export const PROMPTS_QUERY = `
query ViewerPrompts($query: String!) {
prompts(query: $query, first: 100, viewerIsAffiliated: true, orderBy: PROMPT_NAME_WITH_OWNER) {
nodes {
id
name
nameWithOwner
owner {
namespaceName
}
description
draft
definition {
text
}
url
}
totalCount
pageInfo {
hasNextPage
endCursor
}
}
}`

export const REPO_NAME_QUERY = `
query ResolveRepoName($cloneURL: String!) {
repository(cloneURL: $cloneURL) {
Expand Down Expand Up @@ -325,10 +350,10 @@ query SiteIdentification {

export const GET_FEATURE_FLAGS_QUERY = `
query FeatureFlags {
evaluatedFeatureFlags() {
evaluatedFeatureFlags {
name
value
}
}
}
`

Expand Down
47 changes: 30 additions & 17 deletions vscode/src/chat/chat-view/ChatController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,13 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
)
this.disposables.push(this.contextStatusAggregator.addProvider(this.codebaseStatusProvider))

// Keep feature flags updated.
this.disposables.push({
dispose: featureFlagProvider.onFeatureFlagChanged('', () => {
void this.postConfigFeatures()
}),
})

if (this.remoteSearch) {
this.disposables.push(
// Display enhanced context status from the remote search provider
Expand Down Expand Up @@ -568,20 +575,7 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
// Get the latest model list available to the current user to update the ChatModel.
this.handleSetChatModel(getDefaultModelID())

// Get the latest feature config and send to webview.
ClientConfigSingleton.getInstance()
.getConfig()
.then(
async clientConfig =>
await this.postMessage({
type: 'setConfigFeatures',
configFeatures: {
chat: !!clientConfig?.chatEnabled,
attribution: !!clientConfig?.attributionEnabled,
serverSentModels: !!clientConfig?.modelsAPIEnabled,
},
})
)
void this.postConfigFeatures()
}

// When the webview sends the 'ready' message, respond by posting the view config
Expand Down Expand Up @@ -1639,9 +1633,29 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
this.handleGetUserContextFilesCandidates.bind(this)
)
)
this.disposables.push(
handleExtensionAPICallFromWebview(
webviewAPIWrapper,
'queryPrompts',
'queryPrompts/response',
async ({ query }) => {
try {
const prompts = await graphqlClient.queryPrompts(query)
return { result: prompts }
} catch (error) {
return { error: String(error) }
}
}
)
)

const clientConfig = await ClientConfigSingleton.getInstance().getConfig()
await this.postConfigFeatures()

return viewOrPanel
}

private async postConfigFeatures(): Promise<void> {
const clientConfig = await ClientConfigSingleton.getInstance().getConfig()
void this.postMessage({
type: 'setConfigFeatures',
configFeatures: {
Expand All @@ -1653,9 +1667,8 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
attribution: clientConfig?.attributionEnabled ?? false,
serverSentModels: clientConfig?.modelsAPIEnabled ?? false,
},
exportedFeatureFlags: featureFlagProvider.getExposedExperiments(),
})

return viewOrPanel
}

public async setWebviewView(view: View): Promise<void> {
Expand Down
11 changes: 11 additions & 0 deletions vscode/src/chat/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
EnhancedContextContextT,
MentionQuery,
Model,
Prompt,
RangeData,
SerializedChatMessage,
UserLocalHistory,
Expand Down Expand Up @@ -165,6 +166,10 @@ export type WebviewMessage =
| {
command: 'experimental-unit-test-prompt'
}
| {
command: 'queryPrompts'
query: string
}

/**
* A message sent from the extension host to the webview.
Expand Down Expand Up @@ -206,6 +211,7 @@ export type ExtensionMessage =
attribution: boolean
serverSentModels: boolean
}
exportedFeatureFlags: Record<string, boolean>
}
| {
type: 'allMentionProvidersMetadata'
Expand All @@ -216,6 +222,11 @@ export type ExtensionMessage =
/** An opaque value representing the text editor's state. @see {ChatMessage.editorState} */
editorState?: unknown | undefined | null
}
| {
type: 'queryPrompts/response'
result?: Prompt[] | null | undefined
error?: string | null | undefined
}

interface ExtensionAttributionMessage {
snippet: string
Expand Down
Loading

0 comments on commit 921b68a

Please sign in to comment.