Skip to content

Commit

Permalink
Cody: Improve chat history (#52904)
Browse files Browse the repository at this point in the history
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 <hello@philippspiess.com>
  • Loading branch information
2 people authored and ErikaRS committed Jun 22, 2023
1 parent 723afa2 commit cc3b004
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 7 deletions.
5 changes: 5 additions & 0 deletions client/cody-ui/src/chat/Transcript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down
11 changes: 11 additions & 0 deletions client/cody/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions client/cody/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
33 changes: 30 additions & 3 deletions client/cody/src/chat/ChatViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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()
}
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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<void> {
delete this.chatHistory[chatID]
await this.localStorage.deleteChatHistory(chatID)
this.sendChatHistory()
}

/**
* Loads chat history from local storage
*/
Expand All @@ -588,6 +600,21 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
}
}

/**
* Loads the most recent chat
*/
private async loadRecentChat(): Promise<void> {
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
*/
Expand Down
1 change: 1 addition & 0 deletions client/cody/src/chat/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
3 changes: 3 additions & 0 deletions client/cody/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions client/cody/src/services/LocalStorageProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ export class LocalStorage {
}
}

public async deleteChatHistory(chatID: string): Promise<void> {
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<void> {
try {
await this.storage.update(this.KEY_LOCAL_HISTORY, null)
Expand Down
11 changes: 11 additions & 0 deletions client/cody/test/e2e/history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
1 change: 1 addition & 0 deletions client/cody/test/integration/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export async function beforeIntegrationTest(): Promise<void> {
export async function afterIntegrationTest(): Promise<void> {
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
Expand Down
12 changes: 12 additions & 0 deletions client/cody/webviews/UserHistory.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

.item-button {
display: block;
position: relative;
padding-top: .5rem;
padding-bottom: .5rem;
}
Expand All @@ -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;
Expand Down
30 changes: 26 additions & 4 deletions client/cody/webviews/UserHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,25 @@ export const UserHistory: React.FunctionComponent<React.PropsWithChildren<Histor
setView,
vscodeAPI,
}) => {
const onDeleteHistoryItemClick = useCallback(
(event: React.MouseEvent<HTMLElement, 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 })
Expand All @@ -50,7 +62,7 @@ export const UserHistory: React.FunctionComponent<React.PropsWithChildren<Histor
className={styles.clearButton}
type="button"
onClick={onRemoveHistoryClick}
disabled={userHistory === null}
disabled={!userHistory || !Object.keys(userHistory).length}
>
Clear History
</VSCodeButton>
Expand All @@ -64,8 +76,7 @@ export const UserHistory: React.FunctionComponent<React.PropsWithChildren<Histor
+new Date(b[1].lastInteractionTimestamp) - +new Date(a[1].lastInteractionTimestamp)
)
.map(chat => {
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
}
Expand All @@ -79,6 +90,17 @@ export const UserHistory: React.FunctionComponent<React.PropsWithChildren<Histor
>
<div className={styles.itemButtonInnerContainer}>
<div className={styles.itemDate}>{new Date(chat[0]).toLocaleString()}</div>
<div className={styles.itemDelete}>
<VSCodeButton
appearance="icon"
type="button"
onClick={event => {
onDeleteHistoryItemClick(event, chat[0])
}}
>
<i className="codicon codicon-trash" />
</VSCodeButton>
</div>
<div className={styles.itemLastMessage}>{lastMessage.displayText}</div>
</div>
</VSCodeButton>
Expand Down

0 comments on commit cc3b004

Please sign in to comment.