Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cody: Improve chat history #52904

Merged
merged 14 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
abeatrix marked this conversation as resolved.
Show resolved Hide resolved
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)
abeatrix marked this conversation as resolved.
Show resolved Hide resolved
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 => {
abeatrix marked this conversation as resolved.
Show resolved Hide resolved
onDeleteHistoryItemClick(event, chat[0])
}}
>
<i className="codicon codicon-trash" />
</VSCodeButton>
</div>
<div className={styles.itemLastMessage}>{lastMessage.displayText}</div>
</div>
</VSCodeButton>
Expand Down
Loading