Skip to content

Commit

Permalink
Include draft prompts for viewer (#5856)
Browse files Browse the repository at this point in the history
closes:
https://linear.app/sourcegraph/issue/SRCH-1138/make-your-drafts-be-available-in-the-editor
closes:
https://linear.app/sourcegraph/issue/SRCH-1173/prompts-have-the-option-to-be-set-to-automatically-run-without-the

Blocked on: sourcegraph/sourcegraph#1003

This PR includes the new `includeViewerDrafts: true` argument in prompts
query, added as part of the backend PR. This will allow to list user's
owned draft prompts along with all the non draft prompts accessible to
the user.

This PR also integrates the new `autoSubmit` field on prompts, added as
part of the backend PR. The prompts with autoSubmit set as true will be
automatically executed in one click when the user selected them. For
other only the input box will be updated with the prompt text as
currently.

First the backend change PR should be merged and deployed on both dotcom
and s2 for this PR to be merged.

## Test plan

- Visit the prompts screen and make sure the user's draft prompts are
being listed.

https://www.loom.com/share/4d74ba90ba6643a29d0798ad977a5c39

## Changelog

- List viewer's draft prompts in the Prompts Library.
  • Loading branch information
thenamankumar authored Oct 16, 2024
1 parent 9b9f64c commit c6797fe
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 68 deletions.
96 changes: 56 additions & 40 deletions lib/prompt-editor/src/PromptEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ interface Props extends KeyboardEventPluginProps {

export interface PromptEditorRefAPI {
getSerializedValue(): SerializedPromptEditorValue
setFocus(focus: boolean, options?: { moveCursorToEnd?: boolean }): void
appendText(text: string, ensureWhitespaceBefore?: boolean): void
addMentions(items: ContextItem[]): void
setInitialContextMentions(items: ContextItem[]): void
setFocus(focus: boolean, options?: { moveCursorToEnd?: boolean }, cb?: () => void): void
appendText(text: string, cb?: () => void): void
addMentions(items: ContextItem[], cb?: () => void): void
setInitialContextMentions(items: ContextItem[], cb?: () => void): void
setEditorState(state: SerializedPromptEditorState): void
}

Expand Down Expand Up @@ -92,7 +92,8 @@ export const PromptEditor: FunctionComponent<Props> = ({
}
return toSerializedPromptEditorValue(editorRef.current)
},
setFocus(focus, { moveCursorToEnd } = {}): void {
// biome-ignore lint/style/useDefaultParameterLast:
setFocus(focus, { moveCursorToEnd } = {}, cb): void {
const editor = editorRef.current
if (editor) {
if (focus) {
Expand Down Expand Up @@ -123,24 +124,31 @@ export const PromptEditor: FunctionComponent<Props> = ({
// on initial load, for some reason.
setTimeout(doFocus)
},
{ tag: 'skip-scroll-into-view' }
{ tag: 'skip-scroll-into-view', onUpdate: cb }
)
} else {
editor.blur()
cb?.()
}
} else {
cb?.()
}
},
appendText(text: string, ensureWhitespaceBefore?: boolean): void {
editorRef.current?.update(() => {
const root = $getRoot()
root.selectEnd()
$insertNodes([$createTextNode(`${getWhitespace(root)}${text}`)])
root.selectEnd()
})
appendText(text: string, cb?: () => void): void {
editorRef.current?.update(
() => {
const root = $getRoot()
root.selectEnd()
$insertNodes([$createTextNode(`${getWhitespace(root)}${text}`)])
root.selectEnd()
},
{ onUpdate: cb }
)
},
addMentions(items: ContextItem[]) {
addMentions(items: ContextItem[], cb?: () => void): void {
const editor = editorRef.current
if (!editor) {
cb?.()
return
}

Expand All @@ -161,42 +169,50 @@ export const PromptEditor: FunctionComponent<Props> = ({
})
}
if (ops.create.length === 0) {
cb?.()
return
}

editorRef.current?.update(() => {
const nodesToInsert = lexicalNodesForContextItems(ops.create, {
isFromInitialContext: false,
})
$insertNodes([$createTextNode(getWhitespace($getRoot())), ...nodesToInsert])
const lastNode = nodesToInsert.at(-1)
if (lastNode) {
$selectAfter(lastNode)
}
})
editorRef.current?.update(
() => {
const nodesToInsert = lexicalNodesForContextItems(ops.create, {
isFromInitialContext: false,
})
$insertNodes([$createTextNode(getWhitespace($getRoot())), ...nodesToInsert])
const lastNode = nodesToInsert.at(-1)
if (lastNode) {
$selectAfter(lastNode)
}
},
{ onUpdate: cb }
)
},
setInitialContextMentions(items: ContextItem[]) {
setInitialContextMentions(items: ContextItem[], cb?: () => void): void {
const editor = editorRef.current
if (!editor) {
cb?.()
return
}

editor.update(() => {
if (!hasSetInitialContext.current || isEditorContentOnlyInitialContext(editor)) {
if (isEditorContentOnlyInitialContext(editor)) {
// Only clear in this case so that we don't clobber any text that was
// inserted before initial context was received.
$getRoot().clear()
editor.update(
() => {
if (!hasSetInitialContext.current || isEditorContentOnlyInitialContext(editor)) {
if (isEditorContentOnlyInitialContext(editor)) {
// Only clear in this case so that we don't clobber any text that was
// inserted before initial context was received.
$getRoot().clear()
}
const nodesToInsert = lexicalNodesForContextItems(items, {
isFromInitialContext: true,
})
$setSelection($getRoot().selectStart()) // insert at start
$insertNodes(nodesToInsert)
$selectEnd()
hasSetInitialContext.current = true
}
const nodesToInsert = lexicalNodesForContextItems(items, {
isFromInitialContext: true,
})
$setSelection($getRoot().selectStart()) // insert at start
$insertNodes(nodesToInsert)
$selectEnd()
hasSetInitialContext.current = true
}
})
},
{ onUpdate: cb }
)
},
}),
[]
Expand Down
6 changes: 5 additions & 1 deletion lib/shared/src/sourcegraph-api/graphql/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
HIGHLIGHTED_FILE_QUERY,
LEGACY_CHAT_INTENT_QUERY,
LEGACY_CONTEXT_SEARCH_QUERY,
LEGACY_PROMPTS_QUERY_5_8,
LOG_EVENT_MUTATION,
LOG_EVENT_MUTATION_DEPRECATED,
PACKAGE_LIST_QUERY,
Expand Down Expand Up @@ -413,6 +414,7 @@ export interface Prompt {
}
description?: string
draft: boolean
autoSubmit?: boolean
definition: {
text: string
}
Expand Down Expand Up @@ -1111,8 +1113,10 @@ export class SourcegraphGraphQLAPIClient {
}

public async queryPrompts(query: string, signal?: AbortSignal): Promise<Prompt[]> {
const hasIncludeViewerDraftsArg = await this.isValidSiteVersion({ minimumVersion: '5.9.0' })

const response = await this.fetchSourcegraphAPI<APIResponse<{ prompts: { nodes: Prompt[] } }>>(
PROMPTS_QUERY,
hasIncludeViewerDraftsArg ? PROMPTS_QUERY : LEGACY_PROMPTS_QUERY_5_8,
{ query },
signal
)
Expand Down
35 changes: 34 additions & 1 deletion lib/shared/src/sourcegraph-api/graphql/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,8 @@ query ContextFilters {
}
}`

export const PROMPTS_QUERY = `
// Legacy prompts query supported up to Sourcegraph 5.8.0. Newer versions include the `includeViewerDrafts` argument.
export const LEGACY_PROMPTS_QUERY_5_8 = `
query ViewerPrompts($query: String!) {
prompts(query: $query, first: 100, includeDrafts: false, viewerIsAffiliated: true, orderBy: PROMPT_UPDATED_AT) {
nodes {
Expand Down Expand Up @@ -340,6 +341,38 @@ query ViewerPrompts($query: String!) {
}
}`

export const PROMPTS_QUERY = `
query ViewerPrompts($query: String!) {
prompts(query: $query, first: 100, includeDrafts: false, includeViewerDrafts: true, viewerIsAffiliated: true, orderBy: PROMPT_UPDATED_AT) {
nodes {
id
name
nameWithOwner
owner {
namespaceName
}
description
draft
autoSubmit
definition {
text
}
url
createdBy {
id
username
displayName
avatarURL
}
}
totalCount
pageInfo {
hasNextPage
endCursor
}
}
}`

export const REPO_NAME_QUERY = `
query ResolveRepoName($cloneURL: String!) {
repository(cloneURL: $cloneURL) {
Expand Down
14 changes: 12 additions & 2 deletions vscode/src/chat/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,17 @@ export type WebviewMessage =
snippet: string
}
| { command: 'rpc/request'; message: RequestMessage }
| { command: 'chatSession'; action: 'duplicate' | 'new'; sessionID?: string | undefined | null }
| { command: 'log'; level: 'debug' | 'error'; filterLabel: string; message: string }
| {
command: 'chatSession'
action: 'duplicate' | 'new'
sessionID?: string | undefined | null
}
| {
command: 'log'
level: 'debug' | 'error'
filterLabel: string
message: string
}

export interface SmartApplyResult {
taskId: FixupTaskID
Expand Down Expand Up @@ -161,6 +170,7 @@ export type ExtensionMessage =
addContextItemsToLastHumanInput?: ContextItem[] | null | undefined
appendTextToLastPromptEditor?: string | null | undefined
smartApplyResult?: SmartApplyResult | undefined | null
submitHumanInput?: boolean | undefined | null
}
| ({ type: 'attribution' } & ExtensionAttributionMessage)
| { type: 'rpc/response'; message: ResponseMessage }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export function makeHumanMessageInfo(
if (humanEditorRef.current?.getSerializedValue().text.trim().endsWith('@')) {
humanEditorRef.current?.setFocus(true, { moveCursorToEnd: true })
} else {
humanEditorRef.current?.appendText('@', true)
humanEditorRef.current?.appendText('@')
}
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export const HumanMessageEditor: FunctionComponent<{
if (editorRef.current.getSerializedValue().text.trim().endsWith('@')) {
editorRef.current.setFocus(true, { moveCursorToEnd: true })
} else {
editorRef.current.appendText('@', true)
editorRef.current.appendText('@')
}

const value = editorRef.current.getSerializedValue()
Expand All @@ -256,42 +256,51 @@ export const HumanMessageEditor: FunctionComponent<{
// Set up the message listener so the extension can control the input field.
useClientActionListener(
useCallback<ClientActionListener>(
({ addContextItemsToLastHumanInput, appendTextToLastPromptEditor }) => {
if (addContextItemsToLastHumanInput) {
// Add new context to chat from the "Cody Add Selection to Cody Chat"
// command, etc. Only add to the last human input field.
if (isSent) {
return
}
if (
!addContextItemsToLastHumanInput ||
addContextItemsToLastHumanInput.length === 0
) {
return
({ addContextItemsToLastHumanInput, appendTextToLastPromptEditor, submitHumanInput }) => {
// Add new context to chat from the "Cody Add Selection to Cody Chat"
// command, etc. Only add to the last human input field.
if (isSent) {
return
}

const updates: Promise<unknown>[] = []
const awaitUpdate = () => {
let resolve: (value?: unknown) => void
updates.push(
new Promise(r => {
resolve = r
})
)

return () => {
resolve?.()
}
}

if (addContextItemsToLastHumanInput && addContextItemsToLastHumanInput.length > 0) {
const editor = editorRef.current
if (editor) {
editor.addMentions(addContextItemsToLastHumanInput)
editor.addMentions(addContextItemsToLastHumanInput, awaitUpdate())
editor.setFocus(true)
}
}

if (appendTextToLastPromptEditor) {
// Append text to the last human input field.
if (isSent) {
return
}

// Schedule append text task to the next tick to avoid collisions with
// initial text set (add initial mentions first then append text from prompt)
const onUpdate = awaitUpdate()
requestAnimationFrame(() => {
if (editorRef.current) {
editorRef.current.appendText(appendTextToLastPromptEditor)
editorRef.current.appendText(appendTextToLastPromptEditor, onUpdate)
}
})
}

if (submitHumanInput) {
Promise.all(updates).then(() => onSubmitClick())
}
},
[isSent]
[isSent, onSubmitClick]
)
)

Expand Down
6 changes: 5 additions & 1 deletion vscode/webviews/components/promptList/PromptList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,14 @@ export const PromptList: FC<PromptListProps> = props => {
}

const isPrompt = action.actionType === 'prompt'
const isPromptAutoSubmit = action.actionType === 'prompt' && action.autoSubmit
const isCommand = action.actionType === 'command'
const isBuiltInCommand = isCommand && action.type === 'default'

telemetryRecorder.recordEvent('cody.promptList', 'select', {
metadata: {
isPrompt: isPrompt ? 1 : 0,
isPromptAutoSubmit: isPromptAutoSubmit ? 1 : 0,
isCommand: isCommand ? 1 : 0,
isCommandBuiltin: isBuiltInCommand ? 1 : 0,
isCommandCustom: !isBuiltInCommand ? 1 : 0,
Expand Down Expand Up @@ -160,7 +162,9 @@ export const PromptList: FC<PromptListProps> = props => {
tabIndex={0}
shouldFilter={false}
defaultValue={showInitialSelectedItem ? undefined : 'xxx-no-item'}
className={clsx(styles.list, { [styles.listChips]: appearanceMode === 'chips-list' })}
className={clsx(styles.list, {
[styles.listChips]: appearanceMode === 'chips-list',
})}
>
<CommandList className={className}>
{showSearch && (
Expand Down
5 changes: 4 additions & 1 deletion vscode/webviews/prompts/PromptsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ export function useActionSelect() {
case 'prompt': {
setView(View.Chat)
dispatchClientAction(
{ appendTextToLastPromptEditor: action.definition.text },
{
appendTextToLastPromptEditor: action.definition.text,
submitHumanInput: action.autoSubmit,
},
// Buffer because PromptEditor is not guaranteed to be mounted after the `setView`
// call above, and it needs to be mounted to receive the action.
{ buffer: true }
Expand Down

0 comments on commit c6797fe

Please sign in to comment.