From 53869de8fb0a6151cfb1b7ad6bbb005bd0e0b1f0 Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Sat, 28 Sep 2024 05:03:23 -0700 Subject: [PATCH] pass chat history to webview using webviewAPI, not old postMessage protocol --- agent/src/agent.ts | 5 +- lib/shared/src/misc/rpc/webviewAPI.ts | 8 +- vscode/src/chat/chat-view/ChatController.ts | 21 +----- .../src/chat/chat-view/ChatHistoryManager.ts | 73 +++++++++++------- vscode/src/chat/protocol.ts | 2 - vscode/src/services/LocalStorageProvider.ts | 10 ++- vscode/webviews/App.story.tsx | 17 ----- vscode/webviews/App.tsx | 7 -- vscode/webviews/AppWrapperForTest.tsx | 16 ++++ vscode/webviews/CodyPanel.tsx | 27 +------ vscode/webviews/chat/downloadChatHistory.ts | 29 ++++++++ vscode/webviews/tabs/HistoryTab.story.tsx | 16 ++-- vscode/webviews/tabs/HistoryTab.test.tsx | 22 ++++++ vscode/webviews/tabs/HistoryTab.tsx | 74 ++++++++++++------- vscode/webviews/tabs/TabsBar.tsx | 17 +++-- web/lib/components/CodyWebChat.tsx | 8 +- 16 files changed, 205 insertions(+), 147 deletions(-) create mode 100644 vscode/webviews/chat/downloadChatHistory.ts create mode 100644 vscode/webviews/tabs/HistoryTab.test.tsx diff --git a/agent/src/agent.ts b/agent/src/agent.ts index dc85a4212f2..dd45dc47cd6 100644 --- a/agent/src/agent.ts +++ b/agent/src/agent.ts @@ -1232,7 +1232,10 @@ export class Agent extends MessageHandler implements ExtensionClient { modelID ??= (await firstResultFromOperation(modelsService.getDefaultChatModel())) ?? '' const chatMessages = messages?.map(PromptString.unsafe_deserializeChatMessage) ?? [] const chatBuilder = new ChatBuilder(modelID, chatID, chatMessages) - await chatHistory.saveChat(authStatus, chatBuilder.toSerializedChatTranscript()) + const chat = chatBuilder.toSerializedChatTranscript() + if (chat) { + await chatHistory.saveChat(authStatus, chat) + } return this.createChatPanel( Promise.resolve({ type: 'chat', diff --git a/lib/shared/src/misc/rpc/webviewAPI.ts b/lib/shared/src/misc/rpc/webviewAPI.ts index 70d659fafdf..2fefb0eea2f 100644 --- a/lib/shared/src/misc/rpc/webviewAPI.ts +++ b/lib/shared/src/misc/rpc/webviewAPI.ts @@ -1,6 +1,6 @@ import { Observable } from 'observable-fns' import type { AuthStatus, ResolvedConfiguration } from '../..' -import type { ChatMessage } from '../../chat/transcript/messages' +import type { ChatMessage, UserLocalHistory } from '../../chat/transcript/messages' import type { ContextItem } from '../../codebase-context/messages' import type { CodyCommand } from '../../commands/types' import type { FeatureFlag } from '../../experimentation/FeatureFlagProvider' @@ -62,6 +62,11 @@ export interface WebviewToExtensionAPI { * Observe the current transcript. */ transcript(): Observable + + /** + * The current user's chat history. + */ + userHistory(): Observable } export function createExtensionAPI( @@ -84,6 +89,7 @@ export function createExtensionAPI( resolvedConfig: proxyExtensionAPI(messageAPI, 'resolvedConfig'), authStatus: proxyExtensionAPI(messageAPI, 'authStatus'), transcript: proxyExtensionAPI(messageAPI, 'transcript'), + userHistory: proxyExtensionAPI(messageAPI, 'userHistory'), } } diff --git a/vscode/src/chat/chat-view/ChatController.ts b/vscode/src/chat/chat-view/ChatController.ts index 8b4ecc7f1cf..9c415230029 100644 --- a/vscode/src/chat/chat-view/ChatController.ts +++ b/vscode/src/chat/chat-view/ChatController.ts @@ -253,14 +253,6 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv }) ) ) - - // Observe any changes in chat history and send client notifications to - // the consumer - this.disposables.push( - chatHistory.onHistoryChanged(chatHistory => { - this.postMessage({ type: 'history', localHistory: chatHistory }) - }) - ) } /** @@ -1466,15 +1458,9 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv const authStatus = currentAuthStatus() if (authStatus.authenticated) { // Only try to save if authenticated because otherwise we wouldn't be showing a chat. - const allHistory = await chatHistory.saveChat( - authStatus, - this.chatBuilder.toSerializedChatTranscript() - ) - if (allHistory) { - void this.postMessage({ - type: 'history', - localHistory: allHistory, - }) + const chat = this.chatBuilder.toSerializedChatTranscript() + if (chat) { + await chatHistory.saveChat(authStatus, chat) } } } @@ -1674,6 +1660,7 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv authStatus: () => authStatus, transcript: () => this.chatBuilder.changes.pipe(map(chat => chat.getDehydratedMessages())), + userHistory: () => chatHistory.changes, } ) ) diff --git a/vscode/src/chat/chat-view/ChatHistoryManager.ts b/vscode/src/chat/chat-view/ChatHistoryManager.ts index 78007ec16de..f9c3da6161c 100644 --- a/vscode/src/chat/chat-view/ChatHistoryManager.ts +++ b/vscode/src/chat/chat-view/ChatHistoryManager.ts @@ -1,12 +1,16 @@ -import type { - AccountKeyedChatHistory, - AuthStatus, - AuthenticatedAuthStatus, - SerializedChatTranscript, - UserLocalHistory, +import { + type AccountKeyedChatHistory, + type AuthStatus, + type AuthenticatedAuthStatus, + type SerializedChatTranscript, + type UnauthenticatedAuthStatus, + type UserLocalHistory, + authStatus, + combineLatest, + distinctUntilChanged, + startWith, } from '@sourcegraph/cody-shared' - -import debounce from 'lodash/debounce' +import { type Observable, Subject, map } from 'observable-fns' import * as vscode from 'vscode' import { localStorage } from '../../services/LocalStorageProvider' @@ -24,7 +28,9 @@ class ChatHistoryManager implements vscode.Disposable { } } - public getLocalHistory(authStatus: AuthenticatedAuthStatus): UserLocalHistory | null { + public getLocalHistory( + authStatus: Pick + ): UserLocalHistory | null { return localStorage.getChatHistory(authStatus) } @@ -38,16 +44,12 @@ class ChatHistoryManager implements vscode.Disposable { public async saveChat( authStatus: AuthenticatedAuthStatus, - chat: SerializedChatTranscript | undefined - ): Promise { + chat: SerializedChatTranscript + ): Promise { const history = localStorage.getChatHistory(authStatus) - if (chat === undefined) { - return history - } history.chat[chat.id] = chat await localStorage.setChatHistory(authStatus, history) - this.notifyChatHistoryChanged(authStatus) - return history + this.changeNotifications.next() } public async importChatHistory( @@ -56,29 +58,44 @@ class ChatHistoryManager implements vscode.Disposable { authStatus: AuthStatus ): Promise { await localStorage.importChatHistory(history, merge) - this.notifyChatHistoryChanged(authStatus) + this.changeNotifications.next() } public async deleteChat(authStatus: AuthenticatedAuthStatus, chatID: string): Promise { await localStorage.deleteChatHistory(authStatus, chatID) - this.notifyChatHistoryChanged(authStatus) + this.changeNotifications.next() } // Remove chat history and input history public async clear(authStatus: AuthenticatedAuthStatus): Promise { await localStorage.removeChatHistory(authStatus) - this.notifyChatHistoryChanged(authStatus) - } - - public onHistoryChanged(listener: (chatHistory: UserLocalHistory | null) => any): vscode.Disposable { - return this.historyChanged.event(listener) + this.changeNotifications.next() } - private notifyChatHistoryChanged = debounce( - authStatus => this.historyChanged.fire(this.getLocalHistory(authStatus)), - 100, - { leading: true, trailing: true } - ) + private changeNotifications = new Subject() + public changes: Observable = combineLatest([ + authStatus.pipe( + // Only need to rere-fetch the chat history when the endpoint or username changes for + // authed users (i.e., when they switch to a different account), not when anything else + // in the authStatus might change. + map( + ( + authStatus + ): + | UnauthenticatedAuthStatus + | Pick => + authStatus.authenticated + ? { + authenticated: authStatus.authenticated, + endpoint: authStatus.endpoint, + username: authStatus.username, + } + : authStatus + ), + distinctUntilChanged() + ), + this.changeNotifications.pipe(startWith(undefined)), + ]).pipe(map(([authStatus]) => (authStatus.authenticated ? this.getLocalHistory(authStatus) : null))) } export const chatHistory = new ChatHistoryManager() diff --git a/vscode/src/chat/protocol.ts b/vscode/src/chat/protocol.ts index 282dd99a427..4bb4a542871 100644 --- a/vscode/src/chat/protocol.ts +++ b/vscode/src/chat/protocol.ts @@ -11,7 +11,6 @@ import type { RequestMessage, ResponseMessage, SerializedChatMessage, - UserLocalHistory, } from '@sourcegraph/cody-shared' import type { BillingCategory, BillingProduct } from '@sourcegraph/cody-shared/src/telemetry-v2' @@ -146,7 +145,6 @@ export type ExtensionMessage = isDotComUser: boolean workspaceFolderUris: string[] } - | { type: 'history'; localHistory?: UserLocalHistory | undefined | null } | { /** Used by JetBrains and not VS Code. */ type: 'ui/theme' diff --git a/vscode/src/services/LocalStorageProvider.ts b/vscode/src/services/LocalStorageProvider.ts index 4de3a9cc66a..1d66446dcaa 100644 --- a/vscode/src/services/LocalStorageProvider.ts +++ b/vscode/src/services/LocalStorageProvider.ts @@ -152,14 +152,16 @@ class LocalStorage implements LocalStorageForModelPreferences { await this.set(this.CODY_ENDPOINT_HISTORY, [...historySet], fire) } - public getChatHistory(authStatus: AuthenticatedAuthStatus): UserLocalHistory { + public getChatHistory( + authStatus: Pick + ): UserLocalHistory { const history = this.storage.get(this.KEY_LOCAL_HISTORY, null) const accountKey = getKeyForAuthStatus(authStatus) return history?.[accountKey] ?? { chat: {} } } public async setChatHistory( - authStatus: AuthenticatedAuthStatus, + authStatus: Pick, history: UserLocalHistory ): Promise { try { @@ -324,7 +326,9 @@ class LocalStorage implements LocalStorageForModelPreferences { */ export const localStorage = new LocalStorage() -function getKeyForAuthStatus(authStatus: AuthenticatedAuthStatus): ChatHistoryKey { +function getKeyForAuthStatus( + authStatus: Pick +): ChatHistoryKey { return `${authStatus.endpoint}-${authStatus.username}` } diff --git a/vscode/webviews/App.story.tsx b/vscode/webviews/App.story.tsx index ecb383746c6..41660848770 100644 --- a/vscode/webviews/App.story.tsx +++ b/vscode/webviews/App.story.tsx @@ -44,23 +44,6 @@ const dummyVSCodeAPI: VSCodeWrapper = { workspaceFolderUris: [], isDotComUser: true, }) - cb({ - type: 'history', - localHistory: { - chat: { - a: { - id: 'a', - lastInteractionTimestamp: '2024-03-29', - interactions: [ - { - humanMessage: { speaker: 'human', text: 'Hello, world!' }, - assistantMessage: { speaker: 'assistant', text: 'Hi!' }, - }, - ], - }, - }, - }, - }) if (firstTime) { cb({ type: 'view', view: View.Chat }) firstTime = false diff --git a/vscode/webviews/App.tsx b/vscode/webviews/App.tsx index 0245dca7dfb..bf5d5b3377d 100644 --- a/vscode/webviews/App.tsx +++ b/vscode/webviews/App.tsx @@ -7,7 +7,6 @@ import { type ContextItem, GuardrailsPost, PromptString, - type SerializedChatTranscript, type TelemetryRecorder, } from '@sourcegraph/cody-shared' import type { AuthMethod } from '../src/chat/protocol' @@ -32,8 +31,6 @@ export const App: React.FunctionComponent<{ vscodeAPI: VSCodeWrapper }> = ({ vsc const [transcript, setTranscript] = useState([]) - const [userHistory, setUserHistory] = useState() - const [errorMessages, setErrorMessages] = useState([]) const dispatchClientAction = useClientActionDispatcher() @@ -82,9 +79,6 @@ export const App: React.FunctionComponent<{ vscodeAPI: VSCodeWrapper }> = ({ vsc setView(View.Chat) } break - case 'history': - setUserHistory(Object.values(message.localHistory?.chat ?? {})) - break case 'clientAction': dispatchClientAction(message) break @@ -192,7 +186,6 @@ export const App: React.FunctionComponent<{ vscodeAPI: VSCodeWrapper }> = ({ vsc transcript={transcript} vscodeAPI={vscodeAPI} guardrails={guardrails} - userHistory={userHistory ?? []} smartApplyEnabled={config.config.smartApply} /> )} diff --git a/vscode/webviews/AppWrapperForTest.tsx b/vscode/webviews/AppWrapperForTest.tsx index 100d87f3252..e506cf2456c 100644 --- a/vscode/webviews/AppWrapperForTest.tsx +++ b/vscode/webviews/AppWrapperForTest.tsx @@ -10,6 +10,7 @@ import { type ResolvedConfiguration, SYMBOL_CONTEXT_MENTION_PROVIDER, type SymbolKind, + type UserLocalHistory, getMockedDotComClientModels, promiseFactoryToObservable, } from '@sourcegraph/cody-shared' @@ -96,6 +97,21 @@ export const AppWrapperForTest: FunctionComponent<{ children: ReactNode }> = ({ } satisfies Partial as ResolvedConfiguration), authStatus: () => Observable.of(AUTH_STATUS_FIXTURE_AUTHED), transcript: () => Observable.of(FIXTURE_TRANSCRIPT.explainCode), + userHistory: () => + Observable.of({ + chat: { + a: { + id: 'a', + lastInteractionTimestamp: '2024-03-29', + interactions: [ + { + humanMessage: { speaker: 'human', text: 'Hello, world!' }, + assistantMessage: { speaker: 'assistant', text: 'Hi!' }, + }, + ], + }, + }, + }), }, } satisfies Wrapper['value']>, { diff --git a/vscode/webviews/CodyPanel.tsx b/vscode/webviews/CodyPanel.tsx index ac0af753f27..a4e0d68ad31 100644 --- a/vscode/webviews/CodyPanel.tsx +++ b/vscode/webviews/CodyPanel.tsx @@ -1,6 +1,6 @@ import { type AuthStatus, type ClientCapabilities, CodyIDE } from '@sourcegraph/cody-shared' import type React from 'react' -import { type ComponentProps, type FunctionComponent, useCallback, useRef } from 'react' +import { type ComponentProps, type FunctionComponent, useRef } from 'react' import type { ConfigurationSubsetForWebview, LocalEnv } from '../src/chat/protocol' import styles from './App.module.css' import { Chat } from './Chat' @@ -34,8 +34,7 @@ export const CodyPanel: FunctionComponent< | 'showWelcomeMessage' | 'showIDESnippetActions' | 'smartApplyEnabled' - > & - Pick, 'userHistory'> + > > = ({ view, setView, @@ -50,24 +49,10 @@ export const CodyPanel: FunctionComponent< guardrails, showIDESnippetActions, showWelcomeMessage, - userHistory, smartApplyEnabled, }) => { const tabContainerRef = useRef(null) - // Use native browser download dialog to download chat history as a JSON file. - const onDownloadChatClick = useCallback(() => { - const json = JSON.stringify(userHistory, null, 2) - const blob = new Blob([json], { type: 'application/json' }) - const url = URL.createObjectURL(blob) - const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5) // Format: YYYY-MM-DDTHH-mm - const a = document.createElement('a') // a temporary anchor element - a.href = url - a.download = `cody-chat-history-${timestamp}.json` - a.target = '_blank' - a.click() - }, [userHistory]) - return ( + )} {errorMessages && } @@ -109,7 +89,6 @@ export const CodyPanel: FunctionComponent< setView={setView} webviewType={config.webviewType} multipleWebviewsEnabled={config.multipleWebviewsEnabled} - userHistory={userHistory} /> )} {view === View.Prompts && } diff --git a/vscode/webviews/chat/downloadChatHistory.ts b/vscode/webviews/chat/downloadChatHistory.ts new file mode 100644 index 00000000000..83dda1400c9 --- /dev/null +++ b/vscode/webviews/chat/downloadChatHistory.ts @@ -0,0 +1,29 @@ +import { + type SerializedChatTranscript, + type WebviewToExtensionAPI, + firstResultFromOperation, +} from '@sourcegraph/cody-shared' + +/** + * Use native browser download dialog to download chat history as a JSON file. + */ +export async function downloadChatHistory( + extensionAPI: Pick +): Promise { + const userHistory = await firstResultFromOperation(extensionAPI.userHistory()) + const chatHistory: SerializedChatTranscript[] | null = userHistory + ? Object.values(userHistory.chat) + : null + if (!chatHistory) { + return + } + const json = JSON.stringify(chatHistory, null, 2) + const blob = new Blob([json], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5) // Format: YYYY-MM-DDTHH-mm + const a = document.createElement('a') // a temporary anchor element + a.href = url + a.download = `cody-chat-history-${timestamp}.json` + a.target = '_blank' + a.click() +} diff --git a/vscode/webviews/tabs/HistoryTab.story.tsx b/vscode/webviews/tabs/HistoryTab.story.tsx index cb9f7c76fe7..156ab5eed9b 100644 --- a/vscode/webviews/tabs/HistoryTab.story.tsx +++ b/vscode/webviews/tabs/HistoryTab.story.tsx @@ -1,34 +1,34 @@ import { CodyIDE } from '@sourcegraph/cody-shared' import type { Meta, StoryObj } from '@storybook/react' import { VSCodeStandaloneComponent } from '../storybook/VSCodeStoryDecorator' -import { HistoryTab } from './HistoryTab' +import { HistoryTabWithData } from './HistoryTab' -const meta: Meta = { +const meta: Meta = { title: 'cody/HistoryTab', - component: HistoryTab, + component: HistoryTabWithData, decorators: [VSCodeStandaloneComponent], render: args => (
- +
), } export default meta -type Story = StoryObj +type Story = StoryObj export const Empty: Story = { args: { IDE: CodyIDE.VSCode, setView: () => {}, - userHistory: [], + chats: [], }, } export const SingleDay: Story = { args: { - userHistory: [ + chats: [ { id: '1', interactions: [ @@ -45,7 +45,7 @@ export const SingleDay: Story = { export const MultiDay: Story = { args: { - userHistory: [ + chats: [ { id: '1', interactions: [ diff --git a/vscode/webviews/tabs/HistoryTab.test.tsx b/vscode/webviews/tabs/HistoryTab.test.tsx new file mode 100644 index 00000000000..92754f88e1a --- /dev/null +++ b/vscode/webviews/tabs/HistoryTab.test.tsx @@ -0,0 +1,22 @@ +import { CodyIDE } from '@sourcegraph/cody-shared' +import { render, screen } from '@testing-library/react' +import { describe, expect, test, vi } from 'vitest' +import { AppWrapperForTest } from '../AppWrapperForTest' +import { HistoryTabWithData } from './HistoryTab' + +describe('HistoryTabWithData', () => { + test('renders empty state when there are no non-empty chats', () => { + const mockSetView = vi.fn() + const emptyChats = [ + { id: '1', interactions: [], lastInteractionTimestamp: new Date().toISOString() }, + { id: '2', interactions: [], lastInteractionTimestamp: new Date().toISOString() }, + ] + + render(, { + wrapper: AppWrapperForTest, + }) + + expect(screen.getByText('You have no chat history')).toBeInTheDocument() + expect(screen.getByText('Start a new chat')).toBeInTheDocument() + }) +}) diff --git a/vscode/webviews/tabs/HistoryTab.tsx b/vscode/webviews/tabs/HistoryTab.tsx index 3cf6e959531..00896ae7c02 100644 --- a/vscode/webviews/tabs/HistoryTab.tsx +++ b/vscode/webviews/tabs/HistoryTab.tsx @@ -1,9 +1,11 @@ -import type { CodyIDE, SerializedChatTranscript } from '@sourcegraph/cody-shared' +import type { CodyIDE, SerializedChatTranscript, UserLocalHistory } from '@sourcegraph/cody-shared' +import { useExtensionAPI, useObservable } from '@sourcegraph/prompt-editor' import { HistoryIcon, MessageSquarePlusIcon, MessageSquareTextIcon, TrashIcon } from 'lucide-react' import type React from 'react' import { useCallback, useMemo } from 'react' import type { WebviewType } from '../../src/chat/protocol' import { getRelativeChatPeriod } from '../../src/common/time-date' +import { LoadingDots } from '../chat/components/LoadingDots' import { CollapsiblePanel } from '../components/CollapsiblePanel' import { Button } from '../components/shadcn/ui/button' import { getVSCodeAPI } from '../utils/VSCodeApi' @@ -13,34 +15,53 @@ import { getCreateNewChatCommand } from './utils' interface HistoryTabProps { IDE: CodyIDE setView: (view: View) => void - userHistory: SerializedChatTranscript[] webviewType?: WebviewType | undefined | null multipleWebviewsEnabled?: boolean | undefined | null } -export const HistoryTab: React.FC = ({ - userHistory, - IDE, - webviewType, - multipleWebviewsEnabled, - setView, -}) => { +export const HistoryTab: React.FC = props => { + const userHistory = useUserHistory() + const chats = useMemo( + () => (userHistory ? Object.values(userHistory.chat) : userHistory), + [userHistory] + ) + + return ( +
+ {chats === undefined ? ( + + ) : chats === null ? ( +

History is not available.

+ ) : ( + + )} +
+ ) +} + +export const HistoryTabWithData: React.FC< + HistoryTabProps & { chats: UserLocalHistory['chat'][string][] } +> = ({ IDE, webviewType, multipleWebviewsEnabled, setView, chats }) => { + const nonEmptyChats = useMemo(() => chats.filter(chat => chat.interactions.length > 0), [chats]) + const chatByPeriod = useMemo( () => - userHistory - .filter(chat => chat.interactions.length) - .reverse() - .reduce((acc, chat) => { - const period = getRelativeChatPeriod(new Date(chat.lastInteractionTimestamp)) - acc.set(period, [...(acc.get(period) || []), chat]) - return acc - }, new Map()), - [userHistory] + Array.from( + nonEmptyChats + .filter(chat => chat.interactions.length) + .reverse() + .reduce((acc, chat) => { + const period = getRelativeChatPeriod(new Date(chat.lastInteractionTimestamp)) + acc.set(period, [...(acc.get(period) || []), chat]) + return acc + }, new Map()) + ), + [nonEmptyChats] ) const onDeleteButtonClick = useCallback( (id: string) => { - if (userHistory.find(chat => chat.id === id)) { + if (chats.find(chat => chat.id === id)) { getVSCodeAPI().postMessage({ command: 'command', id: 'cody.chat.history.clear', @@ -48,7 +69,7 @@ export const HistoryTab: React.FC = ({ }) } }, - [userHistory] + [chats] ) const handleStartNewChat = () => { @@ -59,11 +80,9 @@ export const HistoryTab: React.FC = ({ setView(View.Chat) } - const chats = Array.from(chatByPeriod) - return ( -
- {chats.map(([period, chats]) => ( +
+ {chatByPeriod.map(([period, chats]) => ( = ({ ))} - {chats.length === 0 && ( + {nonEmptyChats.length === 0 && (
= ({
) } + +function useUserHistory(): UserLocalHistory | null | undefined { + const userHistory = useExtensionAPI().userHistory + return useObservable(useMemo(() => userHistory(), [userHistory])).value +} diff --git a/vscode/webviews/tabs/TabsBar.tsx b/vscode/webviews/tabs/TabsBar.tsx index 0711739651b..1a46b5d60c4 100644 --- a/vscode/webviews/tabs/TabsBar.tsx +++ b/vscode/webviews/tabs/TabsBar.tsx @@ -22,6 +22,8 @@ import { Kbd } from '../components/Kbd' import { Tooltip, TooltipContent, TooltipTrigger } from '../components/shadcn/ui/tooltip' import { useConfig } from '../utils/useConfig' +import { useExtensionAPI } from '@sourcegraph/prompt-editor' +import { downloadChatHistory } from '../chat/downloadChatHistory' import { Button } from '../components/shadcn/ui/button' import { useFeatureFlag } from '../utils/useFeatureFlags' import styles from './TabsBar.module.css' @@ -31,7 +33,6 @@ interface TabsBarProps { IDE: CodyIDE currentView: View setView: (view: View) => void - onDownloadChatClick?: () => void } type IconComponent = React.ForwardRefExoticComponent< @@ -64,8 +65,8 @@ interface TabConfig { subActions?: TabSubAction[] } -export const TabsBar: React.FC = ({ currentView, setView, IDE, onDownloadChatClick }) => { - const tabItems = useTabs({ IDE, onDownloadChatClick }) +export const TabsBar: React.FC = ({ currentView, setView, IDE }) => { + const tabItems = useTabs({ IDE }) const { config: { webviewType, multipleWebviewsEnabled }, } = useConfig() @@ -317,13 +318,15 @@ TabButton.displayName = 'TabButton' * Returns list of tabs and its sub-action buttons, used later as configuration for * tabs rendering in chat header. */ -function useTabs(input: Pick): TabConfig[] { - const { IDE, onDownloadChatClick } = input +function useTabs(input: Pick): TabConfig[] { + const { IDE } = input const { config: { multipleWebviewsEnabled }, } = useConfig() const isUnifiedPromptsAvailable = useFeatureFlag(FeatureFlag.CodyUnifiedPrompts) + const extensionAPI = useExtensionAPI<'userHistory'>() + return useMemo( () => ( @@ -343,7 +346,7 @@ function useTabs(input: Pick): TabC title: 'Export', Icon: DownloadIcon, command: 'cody.chat.history.export', - callback: onDownloadChatClick, + callback: () => downloadChatHistory(extensionAPI), }, { title: 'Delete all', @@ -399,6 +402,6 @@ function useTabs(input: Pick): TabC : null, ] as (TabConfig | null)[] ).filter(isDefined), - [IDE, onDownloadChatClick, multipleWebviewsEnabled, isUnifiedPromptsAvailable] + [IDE, multipleWebviewsEnabled, isUnifiedPromptsAvailable, extensionAPI] ) } diff --git a/web/lib/components/CodyWebChat.tsx b/web/lib/components/CodyWebChat.tsx index c7cb3375429..30656b83a36 100644 --- a/web/lib/components/CodyWebChat.tsx +++ b/web/lib/components/CodyWebChat.tsx @@ -10,7 +10,6 @@ import { ContextItemSource, PromptString, REMOTE_DIRECTORY_PROVIDER_URI, - type SerializedChatTranscript, isErrorLike, setDisplayPathEnvInfo, } from '@sourcegraph/cody-shared' @@ -112,7 +111,6 @@ const CodyWebPanel: FC = props => { const [transcript, setTranscript] = useState([]) const [config, setConfig] = useState(null) const [view, setView] = useState() - const [userHistory, setUserHistory] = useState() useLayoutEffect(() => { vscodeAPI.onMessage(message => { @@ -145,9 +143,6 @@ const CodyWebPanel: FC = props => { case 'clientAction': dispatchClientAction(message) break - case 'history': - setUserHistory(Object.values(message.localHistory?.chat ?? {})) - break } }) }, [vscodeAPI, dispatchClientAction]) @@ -234,7 +229,7 @@ const CodyWebPanel: FC = props => { } }, [initialContextData]) - const isLoading = !config || !view || !userHistory + const isLoading = !config || !view return (
@@ -248,7 +243,6 @@ const CodyWebPanel: FC = props => { setErrorMessages={setErrorMessages} attributionEnabled={false} configuration={config} - userHistory={userHistory} chatEnabled={true} showWelcomeMessage={true} showIDESnippetActions={false}