From cc3b004860fde90adfb963a4aa98e025d63b2d2e Mon Sep 17 00:00:00 2001 From: Deepak Kumar Date: Mon, 12 Jun 2023 16:43:23 +0530 Subject: [PATCH] Cody: Improve chat history (#52904) This PR closes #52544, #52848, #52889 and #52890. The issues mostly overlapped, so I handled it in this PR. Changes made: 1. Add history to the history list on each new conversation started. 2. Display the last human message in history instead of the assistant message. 3. Add a delete button to delete individual chats from history. 4. Load the most recent chat, if available, from the history. 5. Fix the loading of context files. 6. Load the history messages to the end of the scroll position. ## Test plan All E2E tests have passed. https://www.loom.com/share/3dca98dd0db94c298df38ab79c5e72d1 --------- Co-authored-by: Philipp Spiess --- client/cody-ui/src/chat/Transcript.tsx | 5 +++ client/cody/CHANGELOG.md | 11 +++++++ client/cody/package.json | 4 +++ client/cody/src/chat/ChatViewProvider.ts | 33 +++++++++++++++++-- client/cody/src/chat/protocol.ts | 1 + client/cody/src/main.ts | 3 ++ .../cody/src/services/LocalStorageProvider.ts | 12 +++++++ client/cody/test/e2e/history.test.ts | 11 +++++++ client/cody/test/integration/helpers.ts | 1 + client/cody/webviews/UserHistory.module.css | 12 +++++++ client/cody/webviews/UserHistory.tsx | 30 ++++++++++++++--- 11 files changed, 116 insertions(+), 7 deletions(-) diff --git a/client/cody-ui/src/chat/Transcript.tsx b/client/cody-ui/src/chat/Transcript.tsx index 436ed2dba576..acc650854fb6 100644 --- a/client/cody-ui/src/chat/Transcript.tsx +++ b/client/cody-ui/src/chat/Transcript.tsx @@ -76,6 +76,11 @@ export const Transcript: React.FunctionComponent< top: transcriptContainerRef.current.scrollHeight, }) } + + // scroll to the end when messages are loaded + transcriptContainerRef.current.scrollTo({ + top: transcriptContainerRef.current.scrollHeight, + }) } }, [transcript, transcriptContainerRef]) diff --git a/client/cody/CHANGELOG.md b/client/cody/CHANGELOG.md index 7431bf617e4a..cdc7882cb181 100644 --- a/client/cody/CHANGELOG.md +++ b/client/cody/CHANGELOG.md @@ -8,6 +8,17 @@ Starting from `0.2.0`, Cody is using `major.EVEN_NUMBER.patch` for release versi ### Added +- Add delete button for removing individual history [pull/52904](https://github.com/sourcegraph/sourcegraph/pull/52904) +- Load the recent ongoing chat on reload of window [pull/52904](https://github.com/sourcegraph/sourcegraph/pull/52904) + +### Fixed + +- Fix the loading of files and scroll chat to the end while restoring the history [pull/52904](https://github.com/sourcegraph/sourcegraph/pull/52904) + +### Changed + +- Save the current ongoing conversation to the chat history [pull/52904](https://github.com/sourcegraph/sourcegraph/pull/52904) + ### Fixed - Open file paths from Cody's responses in a workspace with the correct protocol. [pull/53103](https://github.com/sourcegraph/sourcegraph/pull/53103) diff --git a/client/cody/package.json b/client/cody/package.json index 67e381fb13a5..4d49c618efdc 100644 --- a/client/cody/package.json +++ b/client/cody/package.json @@ -285,6 +285,10 @@ "command": "cody.delete-access-token", "title": "Cody: Sign out" }, + { + "command": "cody.clear-chat-history", + "title": "Cody: Clear chat history" + }, { "command": "cody.manual-completions", "title": "Cody: Open Completions Panel" diff --git a/client/cody/src/chat/ChatViewProvider.ts b/client/cody/src/chat/ChatViewProvider.ts index 2e8f22a5db25..64f5aac30b59 100644 --- a/client/cody/src/chat/ChatViewProvider.ts +++ b/client/cody/src/chat/ChatViewProvider.ts @@ -157,15 +157,13 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp /** * Restores a session from a chatID - * We delete the loaded session from our in-memory chatHistory (to hide it from the history view) - * but don't modify the localStorage as no data changes when a session is restored */ public async restoreSession(chatID: string): Promise { await this.saveTranscriptToChatHistory() this.cancelCompletion() this.currentChatID = chatID this.transcript = Transcript.fromJSON(this.chatHistory[chatID]) - delete this.chatHistory[chatID] + await this.transcript.toJSON() this.sendTranscript() this.sendChatHistory() } @@ -179,6 +177,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp this.publishConfig() this.sendTranscript() this.sendChatHistory() + await this.loadRecentChat() break case 'submit': await this.onHumanMessageSubmitted(message.text, message.submitType) @@ -217,6 +216,9 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp case 'restoreHistory': await this.restoreSession(message.chatID) break + case 'deleteHistory': + await this.deleteHistory(message.chatID) + break case 'links': void this.openExternalLinks(message.value) break @@ -333,6 +335,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp this.cancelCompletionCallback = null this.sendTranscript() void this.saveTranscriptToChatHistory() + this.sendChatHistory() void vscode.commands.executeCommand('setContext', 'cody.reply.pending', false) this.logEmbeddingsSearchErrors() } @@ -577,6 +580,15 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp this.setWebviewView('login') } + /** + * Delete history from current chat history and local storage + */ + private async deleteHistory(chatID: string): Promise { + delete this.chatHistory[chatID] + await this.localStorage.deleteChatHistory(chatID) + this.sendChatHistory() + } + /** * Loads chat history from local storage */ @@ -588,6 +600,21 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp } } + /** + * Loads the most recent chat + */ + private async loadRecentChat(): Promise { + const localHistory = this.localStorage.getChatHistory() + if (localHistory) { + const chats = localHistory.chat + const sortedChats = Object.entries(chats).sort( + (a, b) => +new Date(b[1].lastInteractionTimestamp) - +new Date(a[1].lastInteractionTimestamp) + ) + const chatID = sortedChats[0][0] + await this.restoreSession(chatID) + } + } + /** * Sends chat history to webview */ diff --git a/client/cody/src/chat/protocol.ts b/client/cody/src/chat/protocol.ts index a9c0340f6f8a..caef935d80f9 100644 --- a/client/cody/src/chat/protocol.ts +++ b/client/cody/src/chat/protocol.ts @@ -17,6 +17,7 @@ export type WebviewMessage = | { command: 'removeToken' } | { command: 'removeHistory' } | { command: 'restoreHistory'; chatID: string } + | { command: 'deleteHistory'; chatID: string } | { command: 'links'; value: string } | { command: 'openFile'; filePath: string } | { command: 'edit'; text: string } diff --git a/client/cody/src/main.ts b/client/cody/src/main.ts index 761c6ae6a75e..e44337bb2ef8 100644 --- a/client/cody/src/main.ts +++ b/client/cody/src/main.ts @@ -175,6 +175,9 @@ const register = async ( vscode.commands.registerCommand('cody.delete-access-token', async () => { await chatProvider.logout() }), + vscode.commands.registerCommand('cody.clear-chat-history', async () => { + await chatProvider.clearHistory() + }), // Commands vscode.commands.registerCommand('cody.welcome', () => vscode.commands.executeCommand('workbench.action.openWalkthrough', 'sourcegraph.cody-ai#welcome', false) diff --git a/client/cody/src/services/LocalStorageProvider.ts b/client/cody/src/services/LocalStorageProvider.ts index 278724eb667d..af1dea25e9f4 100644 --- a/client/cody/src/services/LocalStorageProvider.ts +++ b/client/cody/src/services/LocalStorageProvider.ts @@ -70,6 +70,18 @@ export class LocalStorage { } } + public async deleteChatHistory(chatID: string): Promise { + const userHistory = this.getChatHistory() + if (userHistory) { + try { + delete userHistory.chat[chatID] + await this.storage.update(this.KEY_LOCAL_HISTORY, { ...userHistory }) + } catch (error) { + console.error(error) + } + } + } + public async removeChatHistory(): Promise { try { await this.storage.update(this.KEY_LOCAL_HISTORY, null) diff --git a/client/cody/test/e2e/history.test.ts b/client/cody/test/e2e/history.test.ts index 79e8383ba7ea..61f3c594fab4 100644 --- a/client/cody/test/e2e/history.test.ts +++ b/client/cody/test/e2e/history.test.ts @@ -20,6 +20,17 @@ test('checks for the chat history and new session', async ({ page, sidebar }) => await page.click('[aria-label="Cody: Chat History"]') await expect(sidebar.getByText('Chat History')).toBeVisible() + // start a new chat session and check history + await page.click('[aria-label="Cody: Start a New Chat Session"]') await expect(sidebar.getByText("Hello! I'm Cody. I can write code and answer questions for you.")).toBeVisible() + + await sidebar.getByRole('textbox', { name: 'Text area' }).fill('Hello') + await sidebar.locator('vscode-button').getByRole('img').click() + await expect(sidebar.getByText('Hello')).toBeVisible() + await page.getByRole('button', { name: 'Cody: Chat History' }).click() + await sidebar.locator('vscode-button').filter({ hasText: 'Hello' }).click() + await page.getByRole('button', { name: 'Cody: Chat History' }).click() + await sidebar.locator('vscode-button').filter({ hasText: 'Hello' }).locator('i').click() + await expect(sidebar.getByText('Hello')).not.toBeVisible() }) diff --git a/client/cody/test/integration/helpers.ts b/client/cody/test/integration/helpers.ts index 6c5a4660e3fa..1627d5420667 100644 --- a/client/cody/test/integration/helpers.ts +++ b/client/cody/test/integration/helpers.ts @@ -35,6 +35,7 @@ export async function beforeIntegrationTest(): Promise { export async function afterIntegrationTest(): Promise { await ensureExecuteCommand('cody.delete-access-token') await ensureExecuteCommand('cody.interactive.clear') + await ensureExecuteCommand('cody.clear-chat-history') } // executeCommand specifies ...any[] https://code.visualstudio.com/api/references/vscode-api#commands diff --git a/client/cody/webviews/UserHistory.module.css b/client/cody/webviews/UserHistory.module.css index 53682b6ecdd5..23e20f9a27f7 100644 --- a/client/cody/webviews/UserHistory.module.css +++ b/client/cody/webviews/UserHistory.module.css @@ -22,6 +22,7 @@ .item-button { display: block; + position: relative; padding-top: .5rem; padding-bottom: .5rem; } @@ -37,6 +38,17 @@ margin-bottom: .25rem; } +.item-delete { + position: absolute; + right: 0.25rem; + top: 0.5rem; + visibility: hidden; +} + +.item-button:hover .item-delete { + visibility: visible; +} + .item-last-message { /* Show a few lines of text and then an ellipsis */ display: -webkit-box; diff --git a/client/cody/webviews/UserHistory.tsx b/client/cody/webviews/UserHistory.tsx index 870babeb7068..2d293684220c 100644 --- a/client/cody/webviews/UserHistory.tsx +++ b/client/cody/webviews/UserHistory.tsx @@ -27,13 +27,25 @@ export const UserHistory: React.FunctionComponent { + const onDeleteHistoryItemClick = useCallback( + (event: React.MouseEvent, chatID: string) => { + event.stopPropagation() + if (userHistory) { + delete userHistory[chatID] + setUserHistory({ ...userHistory }) + vscodeAPI.postMessage({ command: 'deleteHistory', chatID }) + } + }, + [userHistory, setUserHistory, vscodeAPI] + ) + const onRemoveHistoryClick = useCallback(() => { if (userHistory) { vscodeAPI.postMessage({ command: 'removeHistory' }) setUserHistory(null) setInputHistory([]) } - }, [setInputHistory, setUserHistory, userHistory, vscodeAPI]) + }, [setInputHistory, userHistory, setUserHistory, vscodeAPI]) function restoreMetadata(chatID: string): void { vscodeAPI.postMessage({ command: 'restoreHistory', chatID }) @@ -50,7 +62,7 @@ export const UserHistory: React.FunctionComponent Clear History @@ -64,8 +76,7 @@ export const UserHistory: React.FunctionComponent { - const lastMessage = - chat[1].interactions[chat[1].interactions.length - 1].assistantMessage + const lastMessage = chat[1].interactions[chat[1].interactions.length - 1].humanMessage if (!lastMessage?.displayText) { return null } @@ -79,6 +90,17 @@ export const UserHistory: React.FunctionComponent
{new Date(chat[0]).toLocaleString()}
+
+ { + onDeleteHistoryItemClick(event, chat[0]) + }} + > + + +
{lastMessage.displayText}