diff --git a/lib/shared/src/misc/rpc/webviewAPI.ts b/lib/shared/src/misc/rpc/webviewAPI.ts index 64723a0d9a14..d3fd1af4475f 100644 --- a/lib/shared/src/misc/rpc/webviewAPI.ts +++ b/lib/shared/src/misc/rpc/webviewAPI.ts @@ -75,7 +75,7 @@ export interface PromptsResult { * `undefined` means that commands should not be shown at all (not even as an empty * list). Builtin and custom commands are deprecated in favor of the Prompt Library. */ - commands: CodyCommand[] + commands: CodyCommand[] | undefined /** The original query used to fetch this result. */ query: string diff --git a/lib/shared/src/sourcegraph-api/graphql/client.ts b/lib/shared/src/sourcegraph-api/graphql/client.ts index 7a526a4d44cc..a376aa46a9e9 100644 --- a/lib/shared/src/sourcegraph-api/graphql/client.ts +++ b/lib/shared/src/sourcegraph-api/graphql/client.ts @@ -421,6 +421,7 @@ export interface Prompt { nameWithOwner: string owner: { namespaceName: string + displayName?: string | null } description?: string draft: boolean diff --git a/vscode/test/e2e/command-core.test.ts b/vscode/test/e2e/command-core.test.ts index 8f9b53b963b3..038185d56173 100644 --- a/vscode/test/e2e/command-core.test.ts +++ b/vscode/test/e2e/command-core.test.ts @@ -106,7 +106,13 @@ test.extend({ '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('ViewerPrompts').replyJson({ + data: { + prompts: { nodes: [] }, + }, + }) + // Sign into Cody await sidebarSignin(page, sidebar) diff --git a/vscode/webviews/AppWrapperForTest.tsx b/vscode/webviews/AppWrapperForTest.tsx index b92ae15b5dbf..68fca874c0f7 100644 --- a/vscode/webviews/AppWrapperForTest.tsx +++ b/vscode/webviews/AppWrapperForTest.tsx @@ -14,8 +14,11 @@ import { Observable } from 'observable-fns' import { type ComponentProps, type FunctionComponent, type ReactNode, useMemo } from 'react' import { URI } from 'vscode-uri' import { COMMON_WRAPPERS } from './AppWrapper' -import { FIXTURE_COMMANDS, makePromptsAPIWithData } from './components/promptList/fixtures' -import { FIXTURE_PROMPTS } from './components/promptSelectField/fixtures' +import { + FIXTURE_COMMANDS, + FIXTURE_PROMPTS, + makePromptsAPIWithData, +} from './components/promptList/fixtures' import { ComposedWrappers, type Wrapper } from './utils/composeWrappers' import { TelemetryRecorderContext } from './utils/telemetry' import { ConfigProvider } from './utils/useConfig' diff --git a/vscode/webviews/Chat.story.tsx b/vscode/webviews/Chat.story.tsx index e25eb5ab87ed..90f0777750b4 100644 --- a/vscode/webviews/Chat.story.tsx +++ b/vscode/webviews/Chat.story.tsx @@ -1,3 +1,4 @@ +import { CodyIDE } from '@sourcegraph/cody-shared' import { ExtensionAPIProviderForTestsOnly, MOCK_API } from '@sourcegraph/prompt-editor' import type { Meta, StoryObj } from '@storybook/react' import { Observable } from 'observable-fns' @@ -19,6 +20,7 @@ const meta: Meta = { }, }, args: { + IDE: CodyIDE.VSCode, transcript: FIXTURE_TRANSCRIPT.simple2, messageInProgress: null, chatEnabled: true, diff --git a/vscode/webviews/Chat.tsx b/vscode/webviews/Chat.tsx index 01c565821177..37f7aabf00c4 100644 --- a/vscode/webviews/Chat.tsx +++ b/vscode/webviews/Chat.tsx @@ -15,12 +15,16 @@ import { truncateTextStart } from '@sourcegraph/cody-shared/src/prompt/truncatio import { CHAT_INPUT_TOKEN_BUDGET } from '@sourcegraph/cody-shared/src/token/constants' import styles from './Chat.module.css' import { WelcomeMessage } from './chat/components/WelcomeMessage' +import { useClientActionDispatcher } from './client/clientState' import { ScrollDown } from './components/ScrollDown' -import type { View } from './tabs' +import { PromptList } from './components/promptList/PromptList' +import { onPromptSelectInPanel, onPromptSelectInPanelActionLabels } from './prompts/PromptsTab' +import { View } from './tabs' import { useTelemetryRecorder } from './utils/telemetry' import { useUserAccountInfo } from './utils/useConfig' interface ChatboxProps { + IDE: CodyIDE chatEnabled: boolean messageInProgress: ChatMessage | null transcript: ChatMessage[] @@ -35,6 +39,7 @@ interface ChatboxProps { } export const Chat: React.FunctionComponent> = ({ + IDE, messageInProgress, transcript, vscodeAPI, @@ -196,6 +201,19 @@ export const Chat: React.FunctionComponent } }, []) + const dispatchClientAction = useClientActionDispatcher() + + const handleScrollDownClick = useCallback(() => { + // Scroll to the bottom instead of focus input for unsent message + // it's possible that we just want to scroll to the bottom in case of + // welcome message screen + if (transcript.length === 0) { + return + } + + focusLastHumanMessageEditor() + }, [transcript]) + return ( <> {!chatEnabled && ( @@ -217,11 +235,25 @@ export const Chat: React.FunctionComponent guardrails={guardrails} smartApplyEnabled={smartApplyEnabled} /> + {transcript.length === 0 && ( + setView(View.Prompts)} + onSelect={item => onPromptSelectInPanel(item, setView, dispatchClientAction)} + /> + )} {transcript.length === 0 && showWelcomeMessage && ( )} {scrollableParent && ( - + )} ) diff --git a/vscode/webviews/CodyPanel.tsx b/vscode/webviews/CodyPanel.tsx index 7718100b0d89..5e976c5351b2 100644 --- a/vscode/webviews/CodyPanel.tsx +++ b/vscode/webviews/CodyPanel.tsx @@ -82,6 +82,7 @@ export const CodyPanel: FunctionComponent< {view === View.Chat && ( )} - {view === View.Prompts && } + {view === View.Prompts && ( + + )} {view === View.Account && } {view === View.Settings && } diff --git a/vscode/webviews/chat/cells/messageCell/human/editor/HumanMessageEditor.tsx b/vscode/webviews/chat/cells/messageCell/human/editor/HumanMessageEditor.tsx index 5670a3e80864..960ca2ea9ea5 100644 --- a/vscode/webviews/chat/cells/messageCell/human/editor/HumanMessageEditor.tsx +++ b/vscode/webviews/chat/cells/messageCell/human/editor/HumanMessageEditor.tsx @@ -198,13 +198,6 @@ export const HumanMessageEditor: FunctionComponent<{ [onGapClick] ) - const appendTextToEditor = useCallback((text: string): void => { - if (!editorRef.current) { - throw new Error('No editorRef') - } - editorRef.current.appendText(text) - }, []) - const onMentionClick = useCallback((): void => { if (!editorRef.current) { throw new Error('No editorRef') @@ -254,9 +247,14 @@ export const HumanMessageEditor: FunctionComponent<{ if (isSent) { return } - if (editorRef.current) { - editorRef.current.appendText(appendTextToLastPromptEditor) - } + + // Schedule append text task to the next tick to avoid collisions with + // initial text set (add initial mentions first then append text from prompt) + requestAnimationFrame(() => { + if (editorRef.current) { + editorRef.current.appendText(appendTextToLastPromptEditor) + } + }) } }, [isSent] @@ -329,7 +327,6 @@ export const HumanMessageEditor: FunctionComponent<{ submitState={submitState} onGapClick={onGapClick} focusEditor={focusEditor} - appendTextToEditor={appendTextToEditor} hidden={!focused && isSent} className={styles.toolbar} /> diff --git a/vscode/webviews/chat/cells/messageCell/human/editor/toolbar/Toolbar.tsx b/vscode/webviews/chat/cells/messageCell/human/editor/toolbar/Toolbar.tsx index 59f5cb15d3b8..837381f4463f 100644 --- a/vscode/webviews/chat/cells/messageCell/human/editor/toolbar/Toolbar.tsx +++ b/vscode/webviews/chat/cells/messageCell/human/editor/toolbar/Toolbar.tsx @@ -4,8 +4,6 @@ import clsx from 'clsx' import { type FunctionComponent, useCallback, useMemo } from 'react' import type { UserAccountInfo } from '../../../../../../Chat' import { ModelSelectField } from '../../../../../../components/modelSelectField/ModelSelectField' -import type { PromptOrDeprecatedCommand } from '../../../../../../components/promptList/PromptList' -import { PromptSelectField } from '../../../../../../components/promptSelectField/PromptSelectField' import { useConfig } from '../../../../../../utils/useConfig' import { AddContextButton } from './AddContextButton' import { SubmitButton, type SubmitButtonState } from './SubmitButton' @@ -27,7 +25,6 @@ export const Toolbar: FunctionComponent<{ onGapClick?: () => void focusEditor?: () => void - appendTextToEditor: (text: string) => void hidden?: boolean className?: string @@ -39,7 +36,6 @@ export const Toolbar: FunctionComponent<{ submitState, onGapClick, focusEditor, - appendTextToEditor, hidden, className, }) => { @@ -80,11 +76,6 @@ export const Toolbar: FunctionComponent<{ className="tw-opacity-60 focus-visible:tw-opacity-100 hover:tw-opacity-100 tw-mr-2" /> )} - void - appendTextToEditor: (text: string) => void - className?: string -}> = ({ focusEditor, appendTextToEditor, className }) => { - const onSelect = useCallback( - (item: PromptOrDeprecatedCommand) => { - appendTextToEditor(item.type === 'prompt' ? item.value.definition.text : item.value.prompt) - focusEditor?.() - }, - [appendTextToEditor, focusEditor] - ) - - return -} - const ModelSelectFieldToolbarItem: FunctionComponent<{ userInfo: UserAccountInfo focusEditor?: () => void diff --git a/vscode/webviews/chat/components/WelcomeMessage.test.tsx b/vscode/webviews/chat/components/WelcomeMessage.test.tsx index 68a577fee7de..fc19061243c0 100644 --- a/vscode/webviews/chat/components/WelcomeMessage.test.tsx +++ b/vscode/webviews/chat/components/WelcomeMessage.test.tsx @@ -1,22 +1,9 @@ import { CodyIDE } from '@sourcegraph/cody-shared' import { fireEvent, render, screen } from '@testing-library/react' -import { describe, expect, test, vi } from 'vitest' +import { describe, expect, test } from 'vitest' import { AppWrapperForTest } from '../../AppWrapperForTest' -import { usePromptsQuery } from '../../components/promptList/usePromptsQuery' -import { FIXTURE_PROMPTS } from '../../components/promptSelectField/fixtures' import { WelcomeMessage } from './WelcomeMessage' -vi.mock('../../components/promptList/usePromptsQuery') -vi.mocked(usePromptsQuery).mockReturnValue({ - value: { - query: '', - commands: [], - prompts: { type: 'results', results: [FIXTURE_PROMPTS[0]] }, - }, - done: false, - error: null, -}) - describe('WelcomeMessage', () => { function openCollapsiblePanels(): void { const closedPanelButtons = document.querySelectorAll('button[data-state="closed"]') @@ -32,12 +19,12 @@ describe('WelcomeMessage', () => { // Check common elements expect(screen.getByText(/Chat Help/)).toBeInTheDocument() - expect(screen.getByText(FIXTURE_PROMPTS[0].name)).toBeInTheDocument() // Check elements specific to CodyIDE.VSCode expect(screen.getByText(/To add code context from an editor/)).toBeInTheDocument() expect(screen.getByText(/Start a new chat using/)).toBeInTheDocument() - expect(screen.getByText(/Customize chat settings/)).toBeInTheDocument() + expect(screen.getByText(/Documentation/)).toBeInTheDocument() + expect(screen.getByText(/Help & Support/)).toBeInTheDocument() }) test('renders for CodyIDE.JetBrains', () => { @@ -48,11 +35,11 @@ describe('WelcomeMessage', () => { // Check common elements expect(screen.getByText(/Chat Help/)).toBeInTheDocument() - expect(screen.getByText(FIXTURE_PROMPTS[0].name)).toBeInTheDocument() // Check elements specific to CodyIDE.JetBrains expect(screen.queryByText(/To add code context from an editor/)).not.toBeInTheDocument() expect(screen.queryByText(/Start a new chat using/)).not.toBeInTheDocument() - expect(screen.queryByText(/Customize chat settings/)).not.toBeInTheDocument() + expect(screen.getByText(/Documentation/)).toBeInTheDocument() + expect(screen.getByText(/Help & Support/)).toBeInTheDocument() }) }) diff --git a/vscode/webviews/chat/components/WelcomeMessage.tsx b/vscode/webviews/chat/components/WelcomeMessage.tsx index 13b1bb2cf14f..cb0f1ca3e13b 100644 --- a/vscode/webviews/chat/components/WelcomeMessage.tsx +++ b/vscode/webviews/chat/components/WelcomeMessage.tsx @@ -1,22 +1,13 @@ import { CodyIDE } from '@sourcegraph/cody-shared' -import { - AtSignIcon, - type LucideProps, - MessageSquarePlusIcon, - SettingsIcon, - TextIcon, -} from 'lucide-react' +import { AtSignIcon, type LucideProps, MessageSquarePlusIcon, TextIcon } from 'lucide-react' import type { FunctionComponent } from 'react' import type React from 'react' -import { useClientActionDispatcher } from '../../client/clientState' import { CollapsiblePanel } from '../../components/CollapsiblePanel' import { Kbd } from '../../components/Kbd' -import { PromptListSuitedForNonPopover } from '../../components/promptList/PromptList' -import { onPromptSelectInPanel, onPromptSelectInPanelActionLabels } from '../../prompts/PromptsTab' import type { View } from '../../tabs' const MenuExample: FunctionComponent<{ children: React.ReactNode }> = ({ children }) => ( - + {children} ) @@ -45,57 +36,58 @@ const localStorageKey = 'chat.welcome-message-dismissed' export const WelcomeMessage: FunctionComponent<{ IDE: CodyIDE; setView: (view: View) => void }> = ({ IDE, - setView, }) => { // Remove the old welcome message dismissal key that is no longer used. localStorage.removeItem(localStorageKey) - const dispatchClientAction = useClientActionDispatcher() - return ( -
- - onPromptSelectInPanel(item, setView, dispatchClientAction)} - onSelectActionLabels={onPromptSelectInPanelActionLabels} - telemetryLocation="PromptsTab" - showCommandOrigins={true} - showPromptLibraryUnsupportedMessage={false} - showOnlyPromptInsertableCommands={false} - className="tw-rounded-none" - /> - +
+ {IDE === CodyIDE.VSCode && ( + <> + + Start a new chat using or the + command Cody: New Chat + + + )} + Type to add context to your chat + {IDE === CodyIDE.VSCode && ( <> To add code context from an editor, right click and use{' '} Cody > Add File/Selection to Cody Chat - - Start a new chat using - - - Customize chat settings with the {' '} - button, or see the{' '} - documentation - )} + +
) diff --git a/vscode/webviews/components/UserAvatar.module.css b/vscode/webviews/components/UserAvatar.module.css index bbed74f9720c..e60794fa598e 100644 --- a/vscode/webviews/components/UserAvatar.module.css +++ b/vscode/webviews/components/UserAvatar.module.css @@ -2,8 +2,6 @@ isolation: isolate; display: inline-flex; border-radius: 50%; - background-color: var(--vscode-inputOption-activeBackground); - color: var(--vscode-inputOption-activeForeground); align-items: center; justify-content: center; height: fit-content; diff --git a/vscode/webviews/components/UserAvatar.story.tsx b/vscode/webviews/components/UserAvatar.story.tsx index e71ff13a5727..a446d7cebcb0 100644 --- a/vscode/webviews/components/UserAvatar.story.tsx +++ b/vscode/webviews/components/UserAvatar.story.tsx @@ -22,7 +22,6 @@ export const Image: Story = { user: { username: 'sqs', avatarURL: 'https://avatars.githubusercontent.com/u/1976', - endpoint: '', }, }, } @@ -31,7 +30,6 @@ export const Text1Letter: Story = { args: { user: { username: 'sqs', - endpoint: '', }, }, } @@ -41,7 +39,6 @@ export const Text2Letters: Story = { user: { username: 'sqs', displayName: 'Quinn Slack', - endpoint: '', }, }, } @@ -51,7 +48,6 @@ export const SourcegraphGradientBorder: Story = { user: { username: 'sqs', avatarURL: 'https://avatars.githubusercontent.com/u/1976', - endpoint: '', }, sourcegraphGradientBorder: true, }, diff --git a/vscode/webviews/components/UserAvatar.tsx b/vscode/webviews/components/UserAvatar.tsx index f77328cb8211..55654e39fae4 100644 --- a/vscode/webviews/components/UserAvatar.tsx +++ b/vscode/webviews/components/UserAvatar.tsx @@ -4,7 +4,7 @@ import type { UserAccountInfo } from '../Chat' import styles from './UserAvatar.module.css' interface Props { - user: NonNullable + user: Pick size: number sourcegraphGradientBorder?: boolean className?: string @@ -74,8 +74,9 @@ const InnerUserAvatar: FunctionComponent {getInitials(user?.displayName || user?.username || '')} diff --git a/vscode/webviews/components/promptList/PromptList.story.tsx b/vscode/webviews/components/promptList/PromptList.story.tsx index e84dd9ec084d..62dad039dc47 100644 --- a/vscode/webviews/components/promptList/PromptList.story.tsx +++ b/vscode/webviews/components/promptList/PromptList.story.tsx @@ -1,7 +1,7 @@ import { ExtensionAPIProviderForTestsOnly, MOCK_API } from '@sourcegraph/prompt-editor' import type { Meta, StoryObj } from '@storybook/react' import { VSCodeStandaloneComponent } from '../../storybook/VSCodeStoryDecorator' -import { FIXTURE_PROMPTS } from '../promptSelectField/fixtures' +import { FIXTURE_PROMPTS } from '../promptList/fixtures' import { PromptList } from './PromptList' import { FIXTURE_COMMANDS } from './fixtures' import { makePromptsAPIWithData } from './fixtures' @@ -61,6 +61,22 @@ export const WithOnlyCommands: Story = { ), } +export const WithOnlyPrompts: Story = { + render: args => ( + + + + ), +} + export const ErrorMessage: Story = { render: args => ( void - onSelectActionLabels?: { prompt: SelectActionLabel; command: SelectActionLabel } showSearch?: boolean - showOnlyPromptInsertableCommands?: boolean showInitialSelectedItem?: boolean showPromptLibraryUnsupportedMessage?: boolean showCommandOrigins?: boolean className?: string commandListClassName?: string - telemetryLocation: 'PromptSelectField' | 'PromptsTab' + showSwitchToPromptAction?: boolean + telemetryLocation: 'ChatTab' | 'PromptsTab' + onSelect: (item: PromptOrDeprecatedCommand) => void + onSwitchToPromptsTab?: () => void + onSelectActionLabels?: { prompt: SelectActionLabel; command: SelectActionLabel } }> = ({ onSelect: parentOnSelect, + onSwitchToPromptsTab, onSelectActionLabels, showSearch = true, - showOnlyPromptInsertableCommands, - showInitialSelectedItem = true, + showInitialSelectedItem = false, showPromptLibraryUnsupportedMessage = true, showCommandOrigins = false, className, commandListClassName, + showSwitchToPromptAction = false, telemetryLocation, }) => { const telemetryRecorder = useTelemetryRecorder() @@ -135,22 +146,25 @@ export const PromptList: React.FunctionComponent<{ const endpointURL = new URL(useConfig().authStatus.endpoint) - // Don't show builtin commands to insert in the prompt editor. - const filteredCommands = showOnlyPromptInsertableCommands - ? result?.commands.filter(c => c.type !== 'default') - : result?.commands + const itemClassName = 'tw-border tw-border-border !tw-rounded-lg !tw-p-4' return ( @@ -160,43 +174,12 @@ export const PromptList: React.FunctionComponent<{ onValueChange={setQuery} placeholder="Search..." autoFocus={true} + wrapperClassName="!tw-border-0 tw-mb-3 tw-px-2" + className="!tw-border-border tw-rounded-md focus:!tw-border-ring !tw-py-3" /> )} {result && result.prompts.type !== 'unsupported' && ( - - Prompt Library -
- - - - } - > + {result.prompts.type === 'results' ? ( <> {result.prompts.results.length === 0 && ( @@ -227,6 +210,17 @@ export const PromptList: React.FunctionComponent<{ prompt={prompt} onSelect={onSelect} selectActionLabel={onSelectActionLabels?.prompt} + className={itemClassName} + /> + ))} + {result?.commands?.map(command => ( + ))} @@ -236,36 +230,7 @@ export const PromptList: React.FunctionComponent<{ )} )} - {result && filteredCommands && filteredCommands.length > 0 && ( - - Commands -
- {hasCustomCommands(filteredCommands) && ( - - )} - - } - > - {filteredCommands.map(command => ( - - ))} - - )} + {showPromptLibraryUnsupportedMessage && result && result.prompts.type === 'unsupported' && ( @@ -283,18 +248,25 @@ export const PromptList: React.FunctionComponent<{ Error: {error.message || 'unknown'} )} + + {showSwitchToPromptAction && ( + + + + + )} ) } -function hasCustomCommands(commands: CodyCommand[]): boolean { - return commands.some( - command => - command.type === CustomCommandType.Workspace || command.type === CustomCommandType.User - ) -} - function commandRowValue(row: PromptOrDeprecatedCommand): string { return row.type === 'prompt' ? `prompt-${row.value.id}` : `command-${row.value.key}` } @@ -303,18 +275,24 @@ const PromptCommandItem: FunctionComponent<{ prompt: Prompt onSelect: (value: string) => void selectActionLabel: SelectActionLabel | undefined -}> = ({ prompt, onSelect, selectActionLabel }) => ( + className?: string +}> = ({ prompt, onSelect, selectActionLabel, className }) => ( -
-
- - {prompt.owner.namespaceName} / - {prompt.name} - + +
+
+ {prompt.name} {prompt.draft && ( Draft @@ -322,13 +300,9 @@ const PromptCommandItem: FunctionComponent<{ )}
{prompt.description && ( - - {prompt.description} - + {prompt.description} )}
-
- {selectActionLabel && } ) @@ -337,22 +311,28 @@ const CodyCommandItem: FunctionComponent<{ onSelect: (value: string) => void selectActionLabel: SelectActionLabel | undefined showCommandOrigins: boolean -}> = ({ command, onSelect, selectActionLabel, showCommandOrigins }) => ( + className?: string +}> = ({ command, onSelect, selectActionLabel, showCommandOrigins, className }) => ( -
+
+ +
+
- + {command.type === 'default' ? command.description : command.key} - + {showCommandOrigins && command.type !== 'default' && ( - {command.type === CustomCommandType.User - ? 'Local User Settings' - : 'Workspace Settings'} + Custom Command )}
@@ -362,49 +342,30 @@ const CodyCommandItem: FunctionComponent<{ )}
-
- {selectActionLabel && } ) -/** Indicator for what will occur when a CommandItem is selected. */ -const CommandItemAction: FunctionComponent<{ label: SelectActionLabel; className?: string }> = ({ - label, +const CommandItemIcon: FunctionComponent<{ command: CodyCommand; size: number; className?: string }> = ({ + command, + size, className, -}) => ( - - - - - - {label === 'insert' - ? 'Append prompt text to chat message' - : 'Run command on current selection in editor'} - - -) +}) => { + const Icon = iconForCommand(command) + return +} -/** - * A variant of {@link PromptList} that is visually more suited for a non-popover. - */ -export const PromptListSuitedForNonPopover: FunctionComponent< - Omit, 'showSearch' | 'showInitialSelectedItem'> -> = ({ className, commandListClassName, ...props }) => ( - -) +function iconForCommand(command: CodyCommand): (typeof ICON_KEYWORDS)[number]['icon'] { + return ICON_KEYWORDS.find(icon => command.key.toLowerCase().includes(icon.keyword))?.icon ?? PlayIcon +} + +const ICON_KEYWORDS: { keyword: string; icon: LucideIcon }[] = [ + { keyword: 'edit', icon: PencilIcon }, + { keyword: 'doc', icon: BookOpenIcon }, + { keyword: 'explain', icon: FileQuestionIcon }, + { keyword: 'test', icon: HammerIcon }, + { keyword: 'fix', icon: BugIcon }, + { keyword: 'debug', icon: BugIcon }, + { keyword: 'secur', icon: ShieldCheckIcon }, + { keyword: 'refactor', icon: CombineIcon }, + { keyword: 'review', icon: MessageCircleCode }, +] diff --git a/vscode/webviews/components/promptList/fixtures.ts b/vscode/webviews/components/promptList/fixtures.ts index d41b411548e3..79a1bb7f830c 100644 --- a/vscode/webviews/components/promptList/fixtures.ts +++ b/vscode/webviews/components/promptList/fixtures.ts @@ -1,11 +1,63 @@ import { type CodyCommand, CustomCommandType, + type Prompt, type PromptsResult, type WebviewToExtensionAPI, promiseFactoryToObservable, } from '@sourcegraph/cody-shared' +export const FIXTURE_PROMPTS: Prompt[] = [ + { + id: '1', + name: 'TypeScript Vitest Test', + nameWithOwner: 'alice/TypeScript Vitest Test', + owner: { namespaceName: 'alice', displayName: 'Alice Zhao' }, + description: 'Generate unit tests for a given function', + draft: false, + definition: { text: 'Generate unit tests for vitest' }, + url: 'https://example.com', + }, + { + id: '2', + name: 'Review OpenCtx Provider', + nameWithOwner: 'alice/Review OpenCtx Provider', + owner: { namespaceName: 'alice', displayName: 'Alice Zhao' }, + description: 'Suggest improvements for an OpenCtx provider', + draft: true, + definition: { text: 'Review the following OpenCtx provider code' }, + url: 'https://example.com', + }, + { + id: '3', + name: 'Generate JUnit Integration Test', + nameWithOwner: 'myorg/Generate JUnit Integration Test', + owner: { namespaceName: 'myorg', displayName: 'My Org' }, + draft: false, + definition: { text: 'Generate a JUnit integration test' }, + url: 'https://example.com', + }, + { + id: '4', + name: 'Fix Bazel Build File', + nameWithOwner: 'myorg/Fix Bazel Build File', + owner: { namespaceName: 'myorg', displayName: 'My Org' }, + draft: false, + definition: { text: 'Fix common issues in this Bazel BUILD file' }, + url: 'https://example.com', + }, + { + id: '5', + name: 'Convert from React Class to Function Component', + nameWithOwner: 'abc-corp/Convert from React Class to Function Component', + owner: { namespaceName: 'abc-corp', displayName: 'ABC Corp' }, + description: 'Convert from a React class component to a function component', + draft: false, + definition: { text: 'Convert from a React class component to a function component' }, + url: 'https://example.com', + }, +] + export const FIXTURE_COMMANDS: CodyCommand[] = [ { key: 'edit', diff --git a/vscode/webviews/components/promptSelectField/PromptSelectField.story.tsx b/vscode/webviews/components/promptSelectField/PromptSelectField.story.tsx deleted file mode 100644 index 478b652cb2a0..000000000000 --- a/vscode/webviews/components/promptSelectField/PromptSelectField.story.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react' -import { VSCodeStandaloneComponent } from '../../storybook/VSCodeStoryDecorator' -import { PromptSelectField } from './PromptSelectField' - -const meta: Meta = { - title: 'cody/PromptSelectField', - component: PromptSelectField, - decorators: [ - story =>
{story()}
, - VSCodeStandaloneComponent, - ], - args: { - __storybook__open: true, - onSelect: () => {}, - }, -} - -export default meta - -type Story = StoryObj - -export const Default: Story = { - args: {}, -} diff --git a/vscode/webviews/components/promptSelectField/PromptSelectField.tsx b/vscode/webviews/components/promptSelectField/PromptSelectField.tsx deleted file mode 100644 index 4ed6e9e3fe2c..000000000000 --- a/vscode/webviews/components/promptSelectField/PromptSelectField.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { useCallback } from 'react' -import { useTelemetryRecorder } from '../../utils/telemetry' -import { PromptList, type PromptOrDeprecatedCommand } from '../promptList/PromptList' -import { ToolbarPopoverItem } from '../shadcn/ui/toolbar' -import { cn } from '../shadcn/utils' - -export const PromptSelectField: React.FunctionComponent<{ - onSelect: (item: PromptOrDeprecatedCommand) => void - onCloseByEscape?: () => void - className?: string - - /** For storybooks only. */ - __storybook__open?: boolean -}> = ({ onSelect, onCloseByEscape, className, __storybook__open }) => { - const telemetryRecorder = useTelemetryRecorder() - - const onOpenChange = useCallback( - (open: boolean): void => { - if (open) { - telemetryRecorder.recordEvent('cody.promptSelectField', 'open', {}) - } - }, - [telemetryRecorder.recordEvent] - ) - - const onKeyDown = useCallback( - (event: React.KeyboardEvent) => { - if (event.key === 'Escape') { - onCloseByEscape?.() - } - }, - [onCloseByEscape] - ) - - return ( - ( - { - onSelect(item) - close() - }} - onSelectActionLabels={{ prompt: 'insert', command: 'insert' }} - showSearch={true} - showOnlyPromptInsertableCommands={true} - showPromptLibraryUnsupportedMessage={true} - telemetryLocation="PromptSelectField" - /> - )} - popoverRootProps={{ onOpenChange }} - popoverContentProps={{ - className: 'tw-min-w-[325px] tw-w-[75vw] tw-max-w-[550px] !tw-p-0', - onKeyDown: onKeyDown, - onCloseAutoFocus: event => { - // Prevent the popover trigger from stealing focus after the user selects an - // item. We want the focus to return to the editor. - event.preventDefault() - }, - }} - > - Prompts - - ) -} diff --git a/vscode/webviews/components/promptSelectField/fixtures.ts b/vscode/webviews/components/promptSelectField/fixtures.ts deleted file mode 100644 index 2e39c68b51af..000000000000 --- a/vscode/webviews/components/promptSelectField/fixtures.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Prompt } from '@sourcegraph/cody-shared' - -export const FIXTURE_PROMPTS: Prompt[] = [ - { - id: '1', - name: 'typescript-vitest-test', - nameWithOwner: 'alice/typescript-vitest-test', - owner: { namespaceName: 'alice' }, - description: 'Generate unit tests for a given function', - draft: false, - definition: { text: 'Generate unit tests for vitest' }, - url: 'https://example.com', - }, - { - id: '2', - name: 'review-openctx-provider', - nameWithOwner: 'alice/review-openctx-provider', - owner: { namespaceName: 'alice' }, - description: 'Suggest improvements for an OpenCtx provider', - draft: true, - definition: { text: 'Review the following OpenCtx provider code' }, - url: 'https://example.com', - }, - { - id: '3', - name: 'generate-junit-integration-test', - nameWithOwner: 'myorg/generate-junit-integration-test', - owner: { namespaceName: 'myorg' }, - draft: false, - definition: { text: 'Generate a JUnit integration test' }, - url: 'https://example.com', - }, - { - id: '4', - name: 'fix-bazel-build-file', - nameWithOwner: 'myorg/fix-bazel-build-file', - owner: { namespaceName: 'myorg' }, - draft: false, - definition: { text: 'Fix common issues in this Bazel BUILD file' }, - url: 'https://example.com', - }, - { - id: '5', - name: 'convert-from-react-class-to-fc', - nameWithOwner: 'abc-corp/convert-from-react-class-to-fc', - owner: { namespaceName: 'abc-corp' }, - // Long text to test wrapping. - description: 'Convert from a React class component to a function component', - draft: false, - definition: { text: 'Convert from a React class component to a function component' }, - url: 'https://example.com', - }, -] diff --git a/vscode/webviews/components/shadcn/ui/command.tsx b/vscode/webviews/components/shadcn/ui/command.tsx index 64a7bfeb95e4..d8dde9dd897e 100644 --- a/vscode/webviews/components/shadcn/ui/command.tsx +++ b/vscode/webviews/components/shadcn/ui/command.tsx @@ -20,9 +20,12 @@ Command.displayName = CommandPrimitive.displayName const CommandInput = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( -
+ React.ComponentPropsWithoutRef & { wrapperClassName?: string } +>(({ wrapperClassName, className, ...props }, ref) => ( +
), } + +export const WithOnlyPrompts: Story = { + render: args => ( + + + + ), +} diff --git a/vscode/webviews/prompts/PromptsTab.tsx b/vscode/webviews/prompts/PromptsTab.tsx index 01bfb02d00cc..10cf2521ba69 100644 --- a/vscode/webviews/prompts/PromptsTab.tsx +++ b/vscode/webviews/prompts/PromptsTab.tsx @@ -1,27 +1,26 @@ +import type { CodyIDE } from '@sourcegraph/cody-shared' import type { ComponentProps } from 'react' import { useClientActionDispatcher } from '../client/clientState' -import { - type PromptList, - PromptListSuitedForNonPopover, - type PromptOrDeprecatedCommand, -} from '../components/promptList/PromptList' +import { PromptList, type PromptOrDeprecatedCommand } from '../components/promptList/PromptList' import { View } from '../tabs/types' import { getVSCodeAPI } from '../utils/VSCodeApi' export const PromptsTab: React.FC<{ + IDE: CodyIDE setView: (view: View) => void }> = ({ setView }) => { const dispatchClientAction = useClientActionDispatcher() return (
- onPromptSelectInPanel(item, setView, dispatchClientAction)} onSelectActionLabels={onPromptSelectInPanelActionLabels} showCommandOrigins={true} + showSearch={true} + showInitialSelectedItem={false} + showSwitchToPromptAction={false} showPromptLibraryUnsupportedMessage={true} - showOnlyPromptInsertableCommands={false} telemetryLocation="PromptsTab" - className="tw-border tw-border-border" />
) diff --git a/vscode/webviews/tabs/TabsBar.tsx b/vscode/webviews/tabs/TabsBar.tsx index 4db663bb1fbd..96ed51c5ffc9 100644 --- a/vscode/webviews/tabs/TabsBar.tsx +++ b/vscode/webviews/tabs/TabsBar.tsx @@ -6,10 +6,12 @@ import { BookTextIcon, CircleUserIcon, DownloadIcon, + ExternalLink, HistoryIcon, type LucideProps, MessageSquarePlusIcon, MessagesSquareIcon, + PlusCircle, SettingsIcon, Trash2Icon, } from 'lucide-react' @@ -23,6 +25,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../components/shadcn/ui import { useConfig } from '../utils/useConfig' import { Button } from '../components/shadcn/ui/button' + import styles from './TabsBar.module.css' import { getCreateNewChatCommand } from './utils' @@ -46,6 +49,7 @@ interface TabSubAction { command: string arg?: string | undefined | null callback?: () => void + uri?: string confirmation?: { title: string description: string @@ -142,7 +146,7 @@ export const TabsBar: React.FC = ({ currentView, setView, IDE, onD
{currentViewSubActions.map(subAction => ( - + {subAction.confirmation ? ( = ({ currentView, setView, IDE, onD void @@ -270,6 +276,7 @@ export const TabButton = forwardRef((props, r Icon, isActive, onClick, + uri, title, alwaysShowTitle, tooltipExtra, @@ -277,15 +284,20 @@ export const TabButton = forwardRef((props, r 'data-testid': dataTestId, } = props + const Component = uri ? 'a' : 'button' + return ( - + {title} {tooltipExtra} @@ -313,7 +325,7 @@ TabButton.displayName = 'TabButton' function useTabs(input: Pick): TabConfig[] { const { IDE, onDownloadChatClick } = input const { - config: { multipleWebviewsEnabled }, + config: { multipleWebviewsEnabled, serverEndpoint }, } = useConfig() return useMemo( @@ -368,6 +380,20 @@ function useTabs(input: Pick): TabC title: IDE === CodyIDE.Web ? 'Prompts' : 'Prompts & Commands', Icon: BookTextIcon, changesView: true, + subActions: [ + { + title: 'Create prompt', + Icon: PlusCircle, + command: '', + uri: `${serverEndpoint}prompts/new`, + }, + { + title: 'Open prompts library', + Icon: ExternalLink, + command: '', + uri: `${serverEndpoint}prompts`, + }, + ], }, multipleWebviewsEnabled ? { @@ -388,6 +414,6 @@ function useTabs(input: Pick): TabC : null, ] as (TabConfig | null)[] ).filter(isDefined), - [IDE, onDownloadChatClick, multipleWebviewsEnabled] + [IDE, onDownloadChatClick, multipleWebviewsEnabled, serverEndpoint] ) } diff --git a/web/CHANGELOG.md b/web/CHANGELOG.md index c1837acc713a..b03ab951076c 100644 --- a/web/CHANGELOG.md +++ b/web/CHANGELOG.md @@ -1,3 +1,6 @@ +# 0.7.6 +- The "Prompts" toolbar item in chat is no longer displayed. To use a prompt from the prompt library, select it from the list shown on the chat tab or prompts tab. + ## 0.7.5 - Improve tabs UI layout for Cody Web tabs configuration - Fix Safari not working cursor click on mention panel problem diff --git a/web/package.json b/web/package.json index 1fbfdc301a5b..048de1d5a4f0 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "@sourcegraph/cody-web", - "version": "0.7.5", + "version": "0.7.6", "description": "Cody standalone web app", "license": "Apache-2.0", "repository": {