Skip to content

Commit

Permalink
Cody Web: Add support running prompts from consumer (#6081)
Browse files Browse the repository at this point in the history
Part of
https://linear.app/sourcegraph/issue/SRCH-1290/implement-new-prompt-library-ui

In the new Prompts Library, we're going to have a Cody Web demo staging,
with which you can run your prompt; this means that we have to have some
API to trigger prompts in the Cody Web UI from the Cody Web consumer.

This PR adds this "external API" functionality by exposing the External
API object

## Test plan
- No Changes in the actual Cody shared webview
  • Loading branch information
vovakulikov authored Nov 8, 2024
1 parent 1167681 commit 5711fc8
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 35 deletions.
2 changes: 1 addition & 1 deletion lib/prompt-editor/src/PromptEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export const PromptEditor: FunctionComponent<Props> = ({
// Ensure element is focused in case the editor is empty. Copied
// from LexicalAutoFocusPlugin.
const doFocus = () =>
editor.getRootElement()?.focus({ preventScroll: true })
editor.getRootElement()?.focus({ preventScroll: false })
doFocus()

// HACK(sqs): Needed in VS Code webviews to actually get it to focus
Expand Down
103 changes: 77 additions & 26 deletions vscode/webviews/CodyPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,54 @@
import {
type AuthStatus,
type ChatMessage,
type ClientCapabilitiesWithLegacyFields,
CodyIDE,
FeatureFlag,
type Guardrails,
firstValueFrom,
} from '@sourcegraph/cody-shared'
import { useExtensionAPI, useObservable } from '@sourcegraph/prompt-editor'
import type React from 'react'
import { type ComponentProps, type FunctionComponent, useEffect, useMemo, useRef } from 'react'
import { type FunctionComponent, useEffect, useMemo, useRef } from 'react'
import type { ConfigurationSubsetForWebview, LocalEnv } from '../src/chat/protocol'
import styles from './App.module.css'
import { Chat } from './Chat'
import { useClientActionDispatcher } from './client/clientState'
import { ConnectivityStatusBanner } from './components/ConnectivityStatusBanner'
import { Notices } from './components/Notices'
import { StateDebugOverlay } from './components/StateDebugOverlay'
import { TabContainer, TabRoot } from './components/shadcn/ui/tabs'
import { AccountTab, HistoryTab, PromptsTab, SettingsTab, TabsBar, View } from './tabs'
import type { VSCodeWrapper } from './utils/VSCodeApi'
import { useFeatureFlag } from './utils/useFeatureFlags'
import { TabViewContext } from './utils/useTabView'

interface CodyPanelProps {
view: View
setView: (view: View) => void
configuration: {
config: LocalEnv & ConfigurationSubsetForWebview
clientCapabilities: ClientCapabilitiesWithLegacyFields
authStatus: AuthStatus
}
errorMessages: string[]
attributionEnabled: boolean
chatEnabled: boolean
messageInProgress: ChatMessage | null
transcript: ChatMessage[]
vscodeAPI: Pick<VSCodeWrapper, 'postMessage' | 'onMessage'>
setErrorMessages: (errors: string[]) => void
guardrails?: Guardrails
showWelcomeMessage?: boolean
showIDESnippetActions?: boolean
smartApplyEnabled?: boolean
onExternalApiReady?: (api: CodyExternalApi) => void
}

/**
* The Cody tab panel, with tabs for chat, history, prompts, etc.
*/
export const CodyPanel: FunctionComponent<
{
view: View
setView: (view: View) => void
configuration: {
config: LocalEnv & ConfigurationSubsetForWebview
clientCapabilities: ClientCapabilitiesWithLegacyFields
authStatus: AuthStatus
}
errorMessages: string[]
setErrorMessages: (errors: string[]) => void
attributionEnabled: boolean
} & Pick<
ComponentProps<typeof Chat>,
| 'chatEnabled'
| 'messageInProgress'
| 'transcript'
| 'vscodeAPI'
| 'guardrails'
| 'showWelcomeMessage'
| 'showIDESnippetActions'
| 'smartApplyEnabled'
>
> = ({
export const CodyPanel: FunctionComponent<CodyPanelProps> = ({
view,
setView,
configuration: { config, clientCapabilities, authStatus },
Expand All @@ -59,13 +63,19 @@ export const CodyPanel: FunctionComponent<
showIDESnippetActions,
showWelcomeMessage,
smartApplyEnabled,
onExternalApiReady,
}) => {
const tabContainerRef = useRef<HTMLDivElement>(null)

const externalAPI = useExternalAPI()
const api = useExtensionAPI()
const { value: chatModels } = useObservable(useMemo(() => api.chatModels(), [api.chatModels]))
const isPromptsV2Enabled = useFeatureFlag(FeatureFlag.CodyPromptsV2)

useEffect(() => {
onExternalApiReady?.(externalAPI)
}, [onExternalApiReady, externalAPI])

useEffect(() => {
const subscription = api.clientActionBroadcast().subscribe(action => {
switch (action.type) {
Expand Down Expand Up @@ -126,7 +136,11 @@ export const CodyPanel: FunctionComponent<
/>
)}
{view === View.Prompts && (
<PromptsTab setView={setView} isPromptsV2Enabled={isPromptsV2Enabled} />
<PromptsTab
IDE={clientCapabilities.agentIDE}
setView={setView}
isPromptsV2Enabled={isPromptsV2Enabled}
/>
)}
{view === View.Account && <AccountTab setView={setView} />}
{view === View.Settings && <SettingsTab />}
Expand Down Expand Up @@ -155,3 +169,40 @@ const ErrorBanner: React.FunctionComponent<{ errors: string[]; setErrors: (error
))}
</div>
)

export interface ExternalPrompt {
text: string
autoSubmit: boolean
mode?: ChatMessage['intent']
}

export interface CodyExternalApi {
runPrompt: (action: ExternalPrompt) => Promise<void>
}

function useExternalAPI(): CodyExternalApi {
const dispatchClientAction = useClientActionDispatcher()
const extensionAPI = useExtensionAPI()

return useMemo(
() => ({
runPrompt: async (prompt: ExternalPrompt) => {
const promptEditorState = await firstValueFrom(
extensionAPI.hydratePromptMessage(prompt.text)
)

dispatchClientAction(
{
editorState: promptEditorState,
submitHumanInput: prompt.autoSubmit,
setLastHumanInputIntent: prompt.mode ?? 'chat',
},
// 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 }
)
},
}),
[extensionAPI, dispatchClientAction]
)
}
4 changes: 3 additions & 1 deletion vscode/webviews/chat/components/WelcomeMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export const WelcomeMessage: FunctionComponent<WelcomeMessageProps> = ({

return (
<div className="tw-flex-1 tw-flex tw-flex-col tw-items-start tw-w-full tw-px-8 tw-gap-6 tw-transition-all">
{isPromptsV2Enabled && <PromptMigrationWidget dismissible={true} className="tw-w-full" />}
{isPromptsV2Enabled && IDE !== CodyIDE.Web && (
<PromptMigrationWidget dismissible={true} className="tw-w-full" />
)}
<div className="tw-flex tw-flex-col tw-gap-4 tw-w-full">
<PromptList
showSearch={false}
Expand Down
7 changes: 4 additions & 3 deletions vscode/webviews/prompts/PromptsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@ import { PromptList } from '../components/promptList/PromptList'
import { View } from '../tabs/types'
import { getVSCodeAPI } from '../utils/VSCodeApi'

import { firstValueFrom } from '@sourcegraph/cody-shared'
import { CodyIDE, firstValueFrom } 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'

export const PromptsTab: React.FC<{
IDE: CodyIDE
setView: (view: View) => void
isPromptsV2Enabled?: boolean
}> = ({ setView, isPromptsV2Enabled }) => {
}> = ({ IDE, setView, isPromptsV2Enabled }) => {
const runAction = useActionSelect()

return (
<div className="tw-overflow-auto tw-h-full tw-flex tw-flex-col tw-gap-6">
{isPromptsV2Enabled && (
{isPromptsV2Enabled && IDE !== CodyIDE.Web && (
<PromptMigrationWidget dismissible={false} className={styles.promptMigrationWidget} />
)}
<PromptList
Expand Down
3 changes: 3 additions & 0 deletions web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 0.11.0
- Support an external API for Cody Panel functionality (now you can trigger running prompts outside of Cody Web component)

## 0.10.0
- Prompts UI update (new prompts list, tab and recent prompts popover)
- Improved performance by bypassing rpc messages hydration
Expand Down
16 changes: 14 additions & 2 deletions web/lib/components/CodyWebChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { ComposedWrappers, type Wrapper } from 'cody-ai/webviews/utils/composeWr
import { createWebviewTelemetryRecorder } from 'cody-ai/webviews/utils/telemetry'
import type { Config } from 'cody-ai/webviews/utils/useConfig'

import type { InitialContext } from '../types'
import type { CodyExternalApi, InitialContext } from '../types'

import { useCodyWebAgent } from './use-cody-agent'

Expand All @@ -50,6 +50,14 @@ export interface CodyWebChatProps {
initialContext?: InitialContext
customHeaders?: Record<string, string>
className?: string

/**
* Whenever an external (imperative) Cody Chat API instance is ready,
* for example it gives you ability to run prompt, Note that this handler
* should be memoized and not change between components re-render, otherwise
* it will be stuck in infinite update loop
*/
onExternalApiReady?: (api: CodyExternalApi) => void
}
/**
* The root component node for Cody Web Chat, implements Cody Agent client
Expand All @@ -66,6 +74,7 @@ export const CodyWebChat: FunctionComponent<CodyWebChatProps> = ({
telemetryClientName,
customHeaders,
className,
onExternalApiReady,
}) => {
const { client, vscodeAPI } = useCodyWebAgent({
serverEndpoint,
Expand All @@ -91,6 +100,7 @@ export const CodyWebChat: FunctionComponent<CodyWebChatProps> = ({
vscodeAPI={vscodeAPI}
initialContext={initialContext}
className={styles.container}
onExternalApiReady={onExternalApiReady}
/>
</div>
</AppWrapper>
Expand All @@ -101,10 +111,11 @@ interface CodyWebPanelProps {
vscodeAPI: VSCodeWrapper
initialContext: InitialContext | undefined
className?: string
onExternalApiReady?: (api: CodyExternalApi) => void
}

const CodyWebPanel: FC<CodyWebPanelProps> = props => {
const { vscodeAPI, initialContext: initialContextData, className } = props
const { vscodeAPI, initialContext: initialContextData, className, onExternalApiReady } = props

const dispatchClientAction = useClientActionDispatcher()
const [errorMessages, setErrorMessages] = useState<string[]>([])
Expand Down Expand Up @@ -257,6 +268,7 @@ const CodyWebPanel: FC<CodyWebPanelProps> = props => {
messageInProgress={messageInProgress}
transcript={transcript}
vscodeAPI={vscodeAPI}
onExternalApiReady={onExternalApiReady}
/>
</ComposedWrappers>
</ChatMentionContext.Provider>
Expand Down
2 changes: 1 addition & 1 deletion web/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { CodyWebChat, type CodyWebChatProps } from './components/CodyWebChat'
export { ChatSkeleton } from './components/skeleton/ChatSkeleton'

export type { Repository, InitialContext } from './types'
export type { Repository, InitialContext, CodyExternalApi } from './types'
12 changes: 12 additions & 0 deletions web/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
// Copy of external prompts interface from CodyPanel component since
// type re-exports don't work with Cody Web bundle
export interface ExternalPrompt {
text: string
autoSubmit: boolean
mode?: 'search' | 'chat' | 'edit' | 'insert'
}

export interface CodyExternalApi {
runPrompt: (action: ExternalPrompt) => Promise<void>
}

export interface Repository {
id: string
name: string
Expand Down
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sourcegraph/cody-web",
"version": "0.10.0",
"version": "0.12.0",
"description": "Cody standalone web app",
"license": "Apache-2.0",
"repository": {
Expand Down

0 comments on commit 5711fc8

Please sign in to comment.