From 6df5557fcdb424e0cb2dffec4bb8baf570cae2e7 Mon Sep 17 00:00:00 2001 From: Naman Kumar Date: Wed, 20 Nov 2024 16:32:11 +0530 Subject: [PATCH] Add Prompts Quick Pick --- .../src/sourcegraph-api/graphql/client.ts | 11 ++- .../src/sourcegraph-api/graphql/queries.ts | 2 +- vscode/package.json | 9 +- vscode/src/chat/chat-view/ChatsController.ts | 21 +++++ vscode/src/chat/protocol.ts | 5 ++ vscode/src/edit/input/get-input.ts | 7 +- vscode/src/edit/input/quick-pick.ts | 6 ++ vscode/src/main.ts | 4 +- vscode/src/prompts/manager.ts | 86 +++++++++++++++++++ .../human/editor/HumanMessageEditor.tsx | 51 ++++++++++- vscode/webviews/prompts/PromptsTab.tsx | 17 ++-- 11 files changed, 194 insertions(+), 25 deletions(-) create mode 100644 vscode/src/prompts/manager.ts diff --git a/lib/shared/src/sourcegraph-api/graphql/client.ts b/lib/shared/src/sourcegraph-api/graphql/client.ts index e2689f3cb9c1..327b2f2637e1 100644 --- a/lib/shared/src/sourcegraph-api/graphql/client.ts +++ b/lib/shared/src/sourcegraph-api/graphql/client.ts @@ -1239,11 +1239,13 @@ export class SourcegraphGraphQLAPIClient { first, recommendedOnly, signal, + orderByMultiple, }: { - query: string + query?: string first: number | undefined - recommendedOnly: boolean + recommendedOnly?: boolean signal?: AbortSignal + orderByMultiple?: PromptsOrderBy[] }): Promise { const hasIncludeViewerDraftsArg = await this.isValidSiteVersion({ minimumVersion: '5.9.0' }) @@ -1253,7 +1255,10 @@ export class SourcegraphGraphQLAPIClient { query, first: first ?? 100, recommendedOnly: recommendedOnly, - orderByMultiple: [PromptsOrderBy.PROMPT_RECOMMENDED, PromptsOrderBy.PROMPT_UPDATED_AT], + orderByMultiple: orderByMultiple || [ + PromptsOrderBy.PROMPT_RECOMMENDED, + PromptsOrderBy.PROMPT_UPDATED_AT, + ], }, signal ) diff --git a/lib/shared/src/sourcegraph-api/graphql/queries.ts b/lib/shared/src/sourcegraph-api/graphql/queries.ts index d5948657dcc1..9e829caae0a7 100644 --- a/lib/shared/src/sourcegraph-api/graphql/queries.ts +++ b/lib/shared/src/sourcegraph-api/graphql/queries.ts @@ -383,7 +383,7 @@ export enum PromptsOrderBy { } export const PROMPTS_QUERY = ` -query ViewerPrompts($query: String!, $first: Int!, $recommendedOnly: Boolean!, $orderByMultiple: [PromptsOrderBy!]) { +query ViewerPrompts($query: String, $first: Int!, $recommendedOnly: Boolean!, $orderByMultiple: [PromptsOrderBy!]) { prompts(query: $query, first: $first, includeDrafts: false, recommendedOnly: $recommendedOnly, includeViewerDrafts: true, viewerIsAffiliated: true, orderByMultiple: $orderByMultiple) { nodes { id diff --git a/vscode/package.json b/vscode/package.json index 89c2e8c51504..d578b28c7b1d 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -676,6 +676,11 @@ "key": "alt+tab", "when": "cody.activated && !editorReadonly && config.cody.internal.unstable" }, + { + "command": "cody.command.execute-prompt", + "key": "alt+p", + "when": "cody.activated" + }, { "command": "cody.tutorial.edit", "key": "alt+k", @@ -804,10 +809,6 @@ "command": "cody.command.abort-commit", "when": "cody.activated && cody.isGeneratingCommit" }, - { - "command": "cody.menu.custom-commands", - "when": "cody.activated" - }, { "command": "cody.chat.signIn", "when": "!cody.activated" diff --git a/vscode/src/chat/chat-view/ChatsController.ts b/vscode/src/chat/chat-view/ChatsController.ts index 32f669eadc02..12f3d5880735 100644 --- a/vscode/src/chat/chat-view/ChatsController.ts +++ b/vscode/src/chat/chat-view/ChatsController.ts @@ -7,6 +7,7 @@ import { type ChatClient, DEFAULT_EVENT_SOURCE, type Guardrails, + type PromptMode, authStatus, currentAuthStatus, currentAuthStatusAuthed, @@ -102,6 +103,26 @@ export class ChatsController implements vscode.Disposable { } } + public async executePrompt({ + text, + mode, + autoSubmit, + }: { text: string; mode: PromptMode; autoSubmit: boolean }): Promise { + await vscode.commands.executeCommand('cody.chat.new') + + const webviewPanelOrView = + this.panel.webviewPanelOrView || (await this.panel.createWebviewViewOrPanel()) + + setTimeout( + () => + webviewPanelOrView.webview.postMessage({ + type: 'clientAction', + setPromptAsInput: { text, mode, autoSubmit }, + }), + 1000 + ) + } + public registerViewsAndCommands() { this.disposables.push( vscode.window.registerWebviewViewProvider('cody.chat', this.panel, { diff --git a/vscode/src/chat/protocol.ts b/vscode/src/chat/protocol.ts index fe56898b8d65..615e4e136de1 100644 --- a/vscode/src/chat/protocol.ts +++ b/vscode/src/chat/protocol.ts @@ -7,6 +7,7 @@ import type { CodyIDE, ContextItem, ContextItemSource, + PromptMode, RangeData, RequestMessage, ResponseMessage, @@ -172,6 +173,10 @@ export type ExtensionMessage = setLastHumanInputIntent?: ChatMessage['intent'] | null | undefined smartApplyResult?: SmartApplyResult | undefined | null submitHumanInput?: boolean | undefined | null + setPromptAsInput?: + | { text: string; mode?: PromptMode | undefined | null; autoSubmit: boolean } + | undefined + | null } | ({ type: 'attribution' } & ExtensionAttributionMessage) | { type: 'rpc/response'; message: ResponseMessage } diff --git a/vscode/src/edit/input/get-input.ts b/vscode/src/edit/input/get-input.ts index a9e54732cdf7..cb1810301853 100644 --- a/vscode/src/edit/input/get-input.ts +++ b/vscode/src/edit/input/get-input.ts @@ -314,13 +314,14 @@ export const getInput = async ( const editInput = createQuickPick({ title: activeTitle, placeHolder: 'Enter edit instructions (type @ to include code, ⏎ to submit)', - getItems: () => - getEditInputItems( + getItems: () => { + return getEditInputItems( editInput.input.value, activeRangeItem, activeModelItem, showModelSelector - ), + ) + }, onDidHide: () => editor.setDecorations(PREVIEW_RANGE_DECORATION, []), ...(source === 'menu' ? { diff --git a/vscode/src/edit/input/quick-pick.ts b/vscode/src/edit/input/quick-pick.ts index 486f3d757d6b..7890bb1003f4 100644 --- a/vscode/src/edit/input/quick-pick.ts +++ b/vscode/src/edit/input/quick-pick.ts @@ -21,6 +21,8 @@ interface QuickPickConfiguration { interface QuickPick { input: vscode.QuickPick render: (value: string) => void + setItems: (items: vscode.QuickPickItem[]) => void + hide: () => void } export const createQuickPick = ({ @@ -90,5 +92,9 @@ export const createQuickPick = ({ quickPick.show() }, + setItems: (items: vscode.QuickPickItem[]) => { + quickPick.items = items + }, + hide: () => quickPick.hide(), } } diff --git a/vscode/src/main.ts b/vscode/src/main.ts index 775bdaaa8607..679b67d3f15b 100644 --- a/vscode/src/main.ts +++ b/vscode/src/main.ts @@ -91,6 +91,7 @@ import { PoorMansBash } from './minion/environment' import { CodyProExpirationNotifications } from './notifications/cody-pro-expiration' import { showSetupNotification } from './notifications/setup-notification' import { logDebug, logError } from './output-channel-logger' +import { PromptsManager } from './prompts/manager' import { initVSCodeGitApi } from './repository/git-extension-api' import { authProvider } from './services/AuthProvider' import { charactersLogger } from './services/CharactersLogger' @@ -896,7 +897,8 @@ function registerChat( ghostHintDecorator, extensionClient: platform.extensionClient, }) - disposables.push(ghostHintDecorator, editorManager, new CodeActionProvider()) + const promptsManager = new PromptsManager({ chatsController }) + disposables.push(ghostHintDecorator, editorManager, new CodeActionProvider(), promptsManager) // Register a serializer for reviving the chat panel on reload if (vscode.window.registerWebviewPanelSerializer) { diff --git a/vscode/src/prompts/manager.ts b/vscode/src/prompts/manager.ts new file mode 100644 index 000000000000..005a0db1ff85 --- /dev/null +++ b/vscode/src/prompts/manager.ts @@ -0,0 +1,86 @@ +import { type PromptMode, graphqlClient } from '@sourcegraph/cody-shared' +import * as vscode from 'vscode' +import type { ChatsController } from '../chat/chat-view/ChatsController' +import { createQuickPick } from '../edit/input/quick-pick' + +export class PromptsManager implements vscode.Disposable { + private disposables: vscode.Disposable[] = [] + private chatsController: ChatsController + + constructor(args: { chatsController: ChatsController }) { + this.chatsController = args.chatsController + + const executePrompt = vscode.commands.registerCommand( + 'cody.command.execute-prompt', + this.showPromptsQuickPick + ) + this.disposables.push(executePrompt) + } + + public showPromptsQuickPick = async (args: any): Promise => { + const getItems = async (query?: string) => { + const prompts = await graphqlClient.queryPrompts({ + query: query || '', + first: 10, + recommendedOnly: false, + }) + + return { + items: prompts.map( + prompt => + ({ + label: prompt.name, + description: prompt.description, + value: JSON.stringify({ + id: prompt.id, + text: prompt.definition.text, + mode: prompt.mode, + autoSubmit: prompt.autoSubmit, + }), + }) as vscode.QuickPickItem + ), + } + } + const quickPick = createQuickPick({ + title: 'Prompts', + placeHolder: 'Search a prompt', + getItems, + onDidAccept: async item => { + // execute prompt + console.log(item) + if (!item) { + return + } + + const { + text, + mode, + autoSubmit, + }: { text: string; mode: PromptMode; autoSubmit: boolean } = JSON.parse( + (item as unknown as { value: string }).value + ) + + this.chatsController.executePrompt({ + text, + mode, + autoSubmit, + }) + + quickPick.hide() + }, + onDidChangeValue: async query => { + const { items } = await getItems(query) + quickPick.setItems(items) + }, + }) + + quickPick.render('') + } + + public dispose(): void { + for (const disposable of this.disposables) { + disposable.dispose() + } + this.disposables = [] + } +} diff --git a/vscode/webviews/chat/cells/messageCell/human/editor/HumanMessageEditor.tsx b/vscode/webviews/chat/cells/messageCell/human/editor/HumanMessageEditor.tsx index 94cb886059c9..e02fa7bdbcc3 100644 --- a/vscode/webviews/chat/cells/messageCell/human/editor/HumanMessageEditor.tsx +++ b/vscode/webviews/chat/cells/messageCell/human/editor/HumanMessageEditor.tsx @@ -5,12 +5,15 @@ import { ModelTag, type SerializedPromptEditorState, type SerializedPromptEditorValue, + firstValueFrom, + skipPendingOperation, textContentFromSerializedLexicalNode, } from '@sourcegraph/cody-shared' import { PromptEditor, type PromptEditorRefAPI, useDefaultContextForChat, + useExtensionAPI, } from '@sourcegraph/prompt-editor' import clsx from 'clsx' import { @@ -25,6 +28,7 @@ import { } from 'react' import type { UserAccountInfo } from '../../../../../Chat' import { type ClientActionListener, useClientActionListener } from '../../../../../client/clientState' +import { promptModeToIntent } from '../../../../../prompts/PromptsTab' import { useTelemetryRecorder } from '../../../../../utils/telemetry' import { useExperimentalOneBox } from '../../../../../utils/useExperimentalOneBox' import styles from './HumanMessageEditor.module.css' @@ -276,6 +280,8 @@ export const HumanMessageEditor: FunctionComponent<{ }) }, [telemetryRecorder.recordEvent, isFirstMessage, isSent]) + const extensionAPI = useExtensionAPI() + // Set up the message listener so the extension can control the input field. useClientActionListener( useCallback( @@ -285,6 +291,7 @@ export const HumanMessageEditor: FunctionComponent<{ appendTextToLastPromptEditor, submitHumanInput, setLastHumanInputIntent, + setPromptAsInput, }) => { // Add new context to chat from the "Cody Add Selection to Cody Chat" // command, etc. Only add to the last human input field. @@ -339,13 +346,51 @@ export const HumanMessageEditor: FunctionComponent<{ setSubmitIntent(setLastHumanInputIntent) } - if (submitHumanInput) { + let promptIntent = undefined + + if (setPromptAsInput) { + // set the intent + promptIntent = promptModeToIntent(setPromptAsInput.mode) + + updates.push( + // biome-ignore lint/suspicious/noAsyncPromiseExecutor: + new Promise(async resolve => { + // get initial context + const { initialContext } = await firstValueFrom( + extensionAPI.defaultContext().pipe(skipPendingOperation()) + ) + // hydrate raw prompt text + const promptEditorState = await firstValueFrom( + extensionAPI.hydratePromptMessage(setPromptAsInput.text, initialContext) + ) + + // update editor state + requestAnimationFrame(async () => { + if (editorRef.current) { + await Promise.all([ + editorRef.current.setEditorState(promptEditorState), + editorRef.current.setFocus(true), + ]) + } + resolve() + }) + }) + ) + } + + if (submitHumanInput || setPromptAsInput?.autoSubmit) { Promise.all(updates).then(() => - onSubmitClick(setLastHumanInputIntent || submitIntent, true) + onSubmitClick(promptIntent || setLastHumanInputIntent || submitIntent, true) ) } }, - [isSent, onSubmitClick, submitIntent] + [ + isSent, + onSubmitClick, + submitIntent, + extensionAPI.hydratePromptMessage, + extensionAPI.defaultContext, + ] ) ) diff --git a/vscode/webviews/prompts/PromptsTab.tsx b/vscode/webviews/prompts/PromptsTab.tsx index 3a09323373b0..05f6f3ca287d 100644 --- a/vscode/webviews/prompts/PromptsTab.tsx +++ b/vscode/webviews/prompts/PromptsTab.tsx @@ -5,9 +5,8 @@ import { PromptList } from '../components/promptList/PromptList' import { View } from '../tabs/types' import { getVSCodeAPI } from '../utils/VSCodeApi' -import { CodyIDE, firstValueFrom } from '@sourcegraph/cody-shared' +import { CodyIDE } from '@sourcegraph/cody-shared' import type { PromptMode } from '@sourcegraph/cody-shared/src/sourcegraph-api/graphql/client' -import { useExtensionAPI } from '@sourcegraph/prompt-editor' import { PromptMigrationWidget } from '../components/promptsMigration/PromptsMigration' import styles from './PromptsTab.module.css' @@ -39,7 +38,7 @@ export const PromptsTab: React.FC<{ ) } -const promptModeToIntent = (mode?: PromptMode): ChatMessage['intent'] => { +export const promptModeToIntent = (mode?: PromptMode | undefined | null): ChatMessage['intent'] => { switch (mode) { case 'CHAT': return 'chat' @@ -54,7 +53,6 @@ const promptModeToIntent = (mode?: PromptMode): ChatMessage['intent'] => { export function useActionSelect() { const dispatchClientAction = useClientActionDispatcher() - const extensionAPI = useExtensionAPI() const [lastUsedActions = {}, persistValue] = useLocalStorage>( 'last-used-actions-v2', {} @@ -71,15 +69,14 @@ export function useActionSelect() { switch (action.actionType) { case 'prompt': { setView(View.Chat) - const promptEditorState = await firstValueFrom( - extensionAPI.hydratePromptMessage(action.definition.text) - ) dispatchClientAction( { - editorState: promptEditorState, - setLastHumanInputIntent: promptModeToIntent(action.mode), - submitHumanInput: action.autoSubmit, + setPromptAsInput: { + text: action.definition.text, + mode: action.mode, + autoSubmit: action.autoSubmit || false, + }, }, // Buffer because PromptEditor is not guaranteed to be mounted after the `setView` // call above, and it needs to be mounted to receive the action.