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

fix: migrate to chat history manager #2059

Merged
merged 6 commits into from
Dec 4, 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
1 change: 1 addition & 0 deletions vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This is a log of all notable changes to Cody for VS Code. [Unreleased] changes a
### Fixed

- Embeddings indexes can be generated and stored locally in repositories with a default fetch URL that is not already indexed by sourcegraph.com through the Enhanced Context selector. [pull/2069](https://github.com/sourcegraph/cody/pull/2069)
- Support chat input history on "up" and "down" arrow keys again. [pull/2059](https://github.com/sourcegraph/cody/pull/2059)

### Changed

Expand Down
66 changes: 21 additions & 45 deletions vscode/src/chat/MessageProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,7 @@ import { newInteraction } from '@sourcegraph/cody-shared/src/chat/prompts/utils'
import { Recipe, RecipeID, RecipeType } from '@sourcegraph/cody-shared/src/chat/recipes/recipe'
import { Transcript } from '@sourcegraph/cody-shared/src/chat/transcript'
import { Interaction } from '@sourcegraph/cody-shared/src/chat/transcript/interaction'
import {
ChatEventSource,
ChatHistory,
ChatMessage,
UserLocalHistory,
} from '@sourcegraph/cody-shared/src/chat/transcript/messages'
import { ChatEventSource, ChatMessage, UserLocalHistory } from '@sourcegraph/cody-shared/src/chat/transcript/messages'
import { Typewriter } from '@sourcegraph/cody-shared/src/chat/typewriter'
import { reformatBotMessageForChat, reformatBotMessageForEdit } from '@sourcegraph/cody-shared/src/chat/viewHelpers'
import { annotateAttribution, Guardrails } from '@sourcegraph/cody-shared/src/guardrails'
Expand All @@ -35,6 +30,7 @@ import { telemetryService } from '../services/telemetry'
import { telemetryRecorder } from '../services/telemetry-v2'
import { TestSupport } from '../test-support'

import { chatHistory } from './chat-view/ChatHistoryManager'
import { ContextProvider } from './ContextProvider'
import { countGeneratedCode } from './utils'

Expand Down Expand Up @@ -86,10 +82,6 @@ export abstract class MessageProvider extends MessageHandler implements vscode.D
public sessionID = new Date(Date.now()).toUTCString()
public currentRequestID: string | undefined = undefined

// input and chat history are shared across all MessageProvider instances
protected static inputHistory: string[] = []
protected static chatHistory: ChatHistory = {}

private isMessageInProgress = false
private cancelCompletionCallback: (() => void) | null = null

Expand Down Expand Up @@ -129,15 +121,13 @@ export abstract class MessageProvider extends MessageHandler implements vscode.D
}

protected async init(chatID?: string): Promise<void> {
this.loadChatHistory()
if (chatID) {
await this.restoreSession(chatID)
}
this.sendTranscript()
this.sendHistory()
await this.contextProvider.init()
await this.sendCodyCommands()

if (chatID) {
await this.restoreSession(chatID)
}
}

private get isDotComUser(): boolean {
Expand All @@ -147,8 +137,8 @@ export abstract class MessageProvider extends MessageHandler implements vscode.D

public async clearAndRestartSession(): Promise<void> {
await this.saveTranscriptToChatHistory()
this.createNewChatID()
this.cancelCompletion()
this.createNewChatID()
this.isMessageInProgress = false
this.transcript.reset()
this.handleSuggestions([])
Expand All @@ -159,9 +149,7 @@ export abstract class MessageProvider extends MessageHandler implements vscode.D
}

public async clearHistory(): Promise<void> {
MessageProvider.chatHistory = {}
MessageProvider.inputHistory = []
await localStorage.removeChatHistory()
await chatHistory.clear()
// Reset the current transcript
this.transcript = new Transcript()
await this.clearAndRestartSession()
Expand All @@ -174,10 +162,14 @@ export abstract class MessageProvider extends MessageHandler implements vscode.D
* Restores a session from a chatID
*/
public async restoreSession(chatID: string): Promise<void> {
const history = chatHistory.getChat(chatID)
if (!history || chatID === this.sessionID) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious when chatID == this.sessionID would happen.

return
}
await this.saveTranscriptToChatHistory()
this.cancelCompletion()
this.createNewChatID(chatID)
this.transcript = Transcript.fromJSON(MessageProvider.chatHistory[chatID])
this.transcript = Transcript.fromJSON(history)
this.chatModel = this.transcript.chatModel
await this.transcript.toJSON()
this.sendTranscript()
Expand Down Expand Up @@ -734,7 +726,6 @@ export abstract class MessageProvider extends MessageHandler implements vscode.D
if (this.transcript.isEmpty) {
return
}
MessageProvider.chatHistory[this.sessionID] = await this.transcript.toJSON()
await this.saveChatHistory()
this.sendHistory()
}
Expand All @@ -743,44 +734,29 @@ export abstract class MessageProvider extends MessageHandler implements vscode.D
* Save chat history
*/
private async saveChatHistory(): Promise<void> {
const userHistory = {
chat: MessageProvider.chatHistory,
input: MessageProvider.inputHistory,
}
await localStorage.setChatHistory(userHistory)
const json = await this.transcript.toJSON()
await chatHistory.saveChat(json)
}

/**
* Delete history from current chat history and local storage
*/
protected async deleteHistory(chatID: string): Promise<void> {
delete MessageProvider.chatHistory[chatID]
await localStorage.deleteChatHistory(chatID)
await chatHistory.deleteChat(chatID)
this.sendHistory()
telemetryService.log('CodyVSCodeExtension:deleteChatHistoryButton:clicked', undefined, { hasV2Event: true })
telemetryRecorder.recordEvent('cody.deleteChatHistoryButton', 'clicked')
}

/**
* Loads chat history from local storage
*/
private loadChatHistory(): void {
const localHistory = localStorage.getChatHistory()
if (localHistory) {
MessageProvider.chatHistory = localHistory?.chat
MessageProvider.inputHistory = localHistory.input
}
}

/**
* Export chat history to file system
*/
public async exportHistory(): Promise<void> {
telemetryService.log('CodyVSCodeExtension:exportChatHistoryButton:clicked', undefined, { hasV2Event: true })
telemetryRecorder.recordEvent('cody.exportChatHistoryButton', 'clicked')
const historyJson = MessageProvider.chatHistory
const historyJson = localStorage.getChatHistory()?.chat
const exportPath = await vscode.window.showSaveDialog({ filters: { 'Chat History': ['json'] } })
if (!exportPath) {
if (!exportPath || !historyJson) {
return
}
try {
Expand All @@ -801,10 +777,10 @@ export abstract class MessageProvider extends MessageHandler implements vscode.D
* Send history to view
*/
private sendHistory(): void {
this.handleHistory({
chat: MessageProvider.chatHistory,
input: MessageProvider.inputHistory,
})
const userHistory = chatHistory.localHistory
if (userHistory) {
this.handleHistory(userHistory)
}
}

/**
Expand Down
46 changes: 33 additions & 13 deletions vscode/src/chat/chat-view/ChatHistoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,45 @@ import { UserLocalHistory } from '@sourcegraph/cody-shared/src/chat/transcript/m
import { localStorage } from '../../services/LocalStorageProvider'

export class ChatHistoryManager {
public getChat(sessionID: string): TranscriptJSON | null {
const chatHistory = localStorage.getChatHistory()
if (!chatHistory) {
return null
}
public get localHistory(): UserLocalHistory | null {
return localStorage.getChatHistory()
}

return chatHistory.chat[sessionID]
public getChat(sessionID: string): TranscriptJSON | null {
const chatHistory = this.localHistory
return chatHistory?.chat ? chatHistory.chat[sessionID] : null
}

public async saveChat(chat: TranscriptJSON): Promise<UserLocalHistory> {
let history = localStorage.getChatHistory()
if (!history) {
history = {
chat: {},
input: [],
}
}
const history = localStorage.getChatHistory()
history.chat[chat.id] = chat
await localStorage.setChatHistory(history)
return history
}

public async deleteChat(chatID: string): Promise<void> {
await localStorage.deleteChatHistory(chatID)
}

// HumanInputHistory is the history list when user presses "up" in the chat input box
public async saveHumanInputHistory(input: string): Promise<UserLocalHistory> {
const history = localStorage.getChatHistory()
history.input.push(input)
await localStorage.setChatHistory(history)
return history
}
public getHumanInputHistory(): string[] {
const history = localStorage.getChatHistory()
if (!history) {
return []
}
return history.input
}

// Remove chat history and input history
public async clear(): Promise<void> {
await localStorage.removeChatHistory()
}
}

export const chatHistory = new ChatHistoryManager()
5 changes: 3 additions & 2 deletions vscode/src/chat/chat-view/ChatPanelProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { MessageErrorType, MessageProvider, MessageProviderOptions } from '../Me
import { ConfigurationSubsetForWebview, ExtensionMessage, LocalEnv, WebviewMessage } from '../protocol'

import { getChatPanelTitle } from './chat-helpers'
import { chatHistory } from './ChatHistoryManager'
import { addWebviewViewHTML, CodyChatPanelViewType } from './ChatManager'

export interface ChatViewProviderWebview extends Omit<vscode.Webview, 'postMessage'> {
Expand Down Expand Up @@ -141,7 +142,7 @@ export class ChatPanelProvider extends MessageProvider {
): Promise<void> {
logDebug('ChatPanelProvider:onHumanMessageSubmitted', 'chat', { verbose: { text, submitType } })

MessageProvider.inputHistory.push(text)
await chatHistory.saveHumanInputHistory(text)

if (submitType === 'suggestion') {
const args = { requestID: this.currentRequestID }
Expand Down Expand Up @@ -248,7 +249,7 @@ export class ChatPanelProvider extends MessageProvider {
type: 'history',
messages: userHistory,
})
void this.treeView.updateTree(createCodyChatTreeItems(userHistory))
void this.treeView.updateTree(createCodyChatTreeItems())
}

/**
Expand Down
20 changes: 5 additions & 15 deletions vscode/src/chat/chat-view/ChatPanelsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { featureFlagProvider } from '@sourcegraph/cody-shared/src/experimentatio
import { View } from '../../../webviews/NavBar'
import { LocalEmbeddingsController } from '../../local-context/local-embeddings'
import { logDebug } from '../../log'
import { localStorage } from '../../services/LocalStorageProvider'
import { createCodyChatTreeItems } from '../../services/treeViewItems'
import { TreeViewProvider } from '../../services/TreeViewProvider'
import { AuthStatus } from '../protocol'
Expand Down Expand Up @@ -103,8 +102,6 @@ export class ChatPanelsManager implements vscode.Disposable {
provider.setConfiguration?.(options.contextProvider.config)
})
})

this.updateTreeViewHistory()
}

public async syncAuthStatus(authStatus: AuthStatus): Promise<void> {
Expand All @@ -114,6 +111,7 @@ export class ChatPanelsManager implements vscode.Disposable {
}

await vscode.commands.executeCommand('setContext', CodyChatPanelViewType, authStatus.isLoggedIn)
await this.updateTreeViewHistory()
}

public async getChatPanel(): Promise<IChatPanelProvider> {
Expand Down Expand Up @@ -235,24 +233,16 @@ export class ChatPanelsManager implements vscode.Disposable {
await chatProvider.executeCustomCommand(title, type)
}

private updateTreeViewHistory(): void {
const localHistory = localStorage.getChatHistory()
if (localHistory) {
void this.treeViewProvider.updateTree(
createCodyChatTreeItems({
chat: localHistory?.chat,
input: localHistory.input,
})
)
}
private async updateTreeViewHistory(): Promise<void> {
await this.treeViewProvider.updateTree(createCodyChatTreeItems())
}

public async clearHistory(chatID?: string): Promise<void> {
if (chatID) {
this.disposeProvider(chatID)

await this.activePanelProvider?.clearChatHistory(chatID)
this.updateTreeViewHistory()
await this.updateTreeViewHistory()
return
}

Expand Down Expand Up @@ -316,7 +306,7 @@ export class ChatPanelsManager implements vscode.Disposable {
provider.dispose()
})
this.panelProvidersMap.clear()
this.updateTreeViewHistory()
void this.updateTreeViewHistory()
}

public dispose(): void {
Expand Down
3 changes: 2 additions & 1 deletion vscode/src/chat/chat-view/SidebarChatProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
WebviewMessage,
} from '../protocol'

import { chatHistory } from './ChatHistoryManager'
import { addWebviewViewHTML } from './ChatManager'

export interface SidebarChatWebview extends Omit<vscode.Webview, 'postMessage'> {
Expand Down Expand Up @@ -237,7 +238,7 @@ export class SidebarChatProvider extends MessageProvider implements vscode.Webvi
verbose: { text, submitType, addEnhancedContext },
})

MessageProvider.inputHistory.push(text)
await chatHistory.saveHumanInputHistory(text)

if (submitType === 'suggestion') {
const args = { requestID: this.currentRequestID }
Expand Down
4 changes: 2 additions & 2 deletions vscode/src/chat/chat-view/SimpleChatPanelProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ export class SimpleChatPanelProvider implements vscode.Disposable, IChatPanelPro
type: 'history',
messages: allHistory,
})
await this.treeView.updateTree(createCodyChatTreeItems(allHistory))
await this.treeView.updateTree(createCodyChatTreeItems())
}

public async clearAndRestartSession(): Promise<void> {
Expand Down Expand Up @@ -405,7 +405,7 @@ export class SimpleChatPanelProvider implements vscode.Disposable, IChatPanelPro
const args = { requestID }
telemetryService.log('CodyVSCodeExtension:chatPredictions:used', args, { hasV2Event: true })
}

await this.history.saveHumanInputHistory(text)
this.chatModel.addHumanMessage({ text })
// trigger the context progress indicator
void this.postViewTranscript({ speaker: 'assistant' })
Expand Down
4 changes: 2 additions & 2 deletions vscode/src/services/LocalStorageProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ export class LocalStorage {
await this.storage.update(this.CODY_ENDPOINT_HISTORY, [...historySet])
}

public getChatHistory(): UserLocalHistory | null {
public getChatHistory(): UserLocalHistory {
const history = this.storage.get<UserLocalHistory | null>(this.KEY_LOCAL_HISTORY, null)
return history
return history || { chat: {}, input: [] }
}

public async setChatHistory(history: UserLocalHistory): Promise<void> {
Expand Down
10 changes: 7 additions & 3 deletions vscode/src/services/treeViewItems.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { UserLocalHistory } from '@sourcegraph/cody-shared/src/chat/transcript/messages'
import { FeatureFlag } from '@sourcegraph/cody-shared/src/experimentation/FeatureFlagProvider'

import { getChatPanelTitle } from '../chat/chat-view/chat-helpers'
import { CODY_DOC_URL, CODY_FEEDBACK_URL, DISCORD_URL } from '../chat/protocol'

import { envInit } from './LocalAppDetector'
import { localStorage } from './LocalStorageProvider'

export type CodyTreeItemType = 'command' | 'support' | 'search' | 'chat'

Expand Down Expand Up @@ -37,9 +37,13 @@ export function getCodyTreeItems(type: CodyTreeItemType): CodySidebarTreeItem[]
}

// functon to create chat tree items from user chat history
export function createCodyChatTreeItems(userHistory: UserLocalHistory): CodySidebarTreeItem[] {
export function createCodyChatTreeItems(): CodySidebarTreeItem[] {
const userHistory = localStorage.getChatHistory()?.chat
if (!userHistory) {
return []
}
const chatTreeItems: CodySidebarTreeItem[] = []
const chatHistoryEntries = [...Object.entries(userHistory.chat)]
const chatHistoryEntries = [...Object.entries(userHistory)]
chatHistoryEntries.forEach(([id, entry]) => {
const lastHumanMessage = entry?.interactions?.findLast(interaction => interaction?.humanMessage)
if (lastHumanMessage?.humanMessage.displayText && lastHumanMessage?.humanMessage.text) {
Expand Down
Loading
Loading