Skip to content

Commit

Permalink
fix: chat show up in tree view on submit (#2171)
Browse files Browse the repository at this point in the history
CLOSE #2148
#2150
#2170
#2169

Fix issues where:
- chats don't appear until stream ends (simplified chat) #2148
-  active chats block new chats being started (simplified chat) #2150
- getReadmeContext is slow

Changse included:
- saveSession after human submission instead of only saving chat after
assistant has responded
- combine saveAndPostHistory and saveSession
- limit getReadmeContext to 1 file at root of repo using global pattern

## Test plan

<!-- Required. See
https://docs.sourcegraph.com/dev/background-information/testing_principles.
-->

- ask Cody a question
- before Cody has responded, the tree view has already been updated with
the new session added
- you can start a new chat panel while waiting for Cody to response
- you cannot create multiple empty new chat panels
- you can delete chat history from tree view
- you can press up to loop through your input history


https://github.com/sourcegraph/cody/assets/68532117/2b62cc8e-c5ce-483c-97ed-fcada0bec22a

---------

Co-authored-by: Beyang Liu <[email protected]>
  • Loading branch information
abeatrix and beyang authored Dec 7, 2023
1 parent 13a31d5 commit 5897d2c
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 70 deletions.
12 changes: 9 additions & 3 deletions lib/ui/src/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@ export const Chat: React.FunctionComponent<ChatProps> = ({
} else {
const newInput = selectedCommand?.slashCommand
setFormInput(newInput || formInput)
setDisplayCommands(null)
setSelectedChatCommand(-1)
}
}
return
Expand Down Expand Up @@ -469,7 +471,7 @@ export const Chat: React.FunctionComponent<ChatProps> = ({
}

// Loop through input history on up arrow press
if (!inputHistory.length) {
if (!inputHistory?.length) {
return
}

Expand Down Expand Up @@ -570,7 +572,11 @@ export const Chat: React.FunctionComponent<ChatProps> = ({
)}

<form className={classNames(styles.inputRow, inputRowClassName)}>
{!displayCommands && suggestions !== undefined && suggestions.length !== 0 && SuggestionButton ? (
{!displayCommands &&
!contextSelection &&
suggestions !== undefined &&
suggestions.length !== 0 &&
SuggestionButton ? (
<div className={styles.suggestions}>
{suggestions.map((suggestion: string) =>
suggestion.trim().length > 0 ? (
Expand All @@ -589,7 +595,7 @@ export const Chat: React.FunctionComponent<ChatProps> = ({
</div>
)}
<div className={styles.textAreaContainer}>
{displayCommands && ChatCommandsComponent && formInput && (
{displayCommands && ChatCommandsComponent && formInput.startsWith('/') && (
<ChatCommandsComponent
chatCommands={displayCommands}
selectedChatCommand={selectedChatCommand}
Expand Down
5 changes: 4 additions & 1 deletion vscode/src/chat/chat-view/ChatHistoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ export class ChatHistoryManager {
return chatHistory?.chat ? chatHistory.chat[sessionID] : null
}

public async saveChat(chat: TranscriptJSON): Promise<UserLocalHistory> {
public async saveChat(chat: TranscriptJSON, input?: string): Promise<UserLocalHistory> {
const history = localStorage.getChatHistory()
history.chat[chat.id] = chat
if (input) {
history.input.push(input)
}
await localStorage.setChatHistory(history)
return history
}
Expand Down
2 changes: 0 additions & 2 deletions vscode/src/chat/chat-view/ChatPanelsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,6 @@ export class ChatPanelsManager implements vscode.Disposable {

private disposeProvider(chatID: string): void {
if (chatID === this.activePanelProvider?.sessionID) {
this.activePanelProvider.webviewPanel?.dispose()
this.activePanelProvider.dispose()
this.activePanelProvider = undefined
}

Expand Down
53 changes: 28 additions & 25 deletions vscode/src/chat/chat-view/SimpleChatModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as vscode from 'vscode'
import { ChatError, ChatMessage } from '@sourcegraph/cody-shared'
import { TranscriptJSON } from '@sourcegraph/cody-shared/src/chat/transcript'
import { InteractionJSON } from '@sourcegraph/cody-shared/src/chat/transcript/interaction'
import { errorToChatError } from '@sourcegraph/cody-shared/src/chat/transcript/messages'
import { errorToChatError, InteractionMessage } from '@sourcegraph/cody-shared/src/chat/transcript/messages'
import { reformatBotMessageForChat } from '@sourcegraph/cody-shared/src/chat/viewHelpers'
import { Message } from '@sourcegraph/cody-shared/src/sourcegraph-api'

Expand Down Expand Up @@ -119,30 +119,7 @@ export class SimpleChatModel {
for (let i = 0; i < this.messagesWithContext.length; i += 2) {
const humanMessage = this.messagesWithContext[i]
const botMessage = this.messagesWithContext[i + 1]
if (humanMessage.message.speaker !== 'human') {
throw new Error('SimpleChatModel.toTranscriptJSON: expected human message, got bot')
}
if (botMessage.message.speaker !== 'assistant') {
throw new Error('SimpleChatModel.toTranscriptJSON: expected bot message, got human')
}
interactions.push({
humanMessage: {
speaker: humanMessage.message.speaker,
text: humanMessage.message.text,
displayText: getDisplayText(humanMessage),
},
assistantMessage: {
speaker: botMessage.message.speaker,
text: botMessage.message.text,
displayText: getDisplayText(botMessage),
},
usedContextFiles: contextItemsToContextFiles(humanMessage.newContextUsed ?? []),

// These fields are unused on deserialization
fullContext: [],
usedPreciseContext: [],
timestamp: 'n/a',
})
interactions.push(messageToInteractionJSON(humanMessage, botMessage))
}
return {
id: this.sessionID,
Expand All @@ -153,6 +130,32 @@ export class SimpleChatModel {
}
}

function messageToInteractionJSON(humanMessage: MessageWithContext, botMessage: MessageWithContext): InteractionJSON {
if (humanMessage?.message?.speaker !== 'human') {
throw new Error('SimpleChatModel.toTranscriptJSON: expected human message, got bot')
}
return {
humanMessage: messageToInteractionMessage(humanMessage),
assistantMessage:
botMessage?.message?.speaker === 'assistant'
? messageToInteractionMessage(botMessage)
: { speaker: 'assistant' },
usedContextFiles: contextItemsToContextFiles(humanMessage.newContextUsed ?? []),
// These fields are unused on deserialization
fullContext: [],
usedPreciseContext: [],
timestamp: new Date().toISOString(),
}
}

function messageToInteractionMessage(message: MessageWithContext): InteractionMessage {
return {
speaker: message.message.speaker,
text: message.message.text,
displayText: getDisplayText(message),
}
}

export interface ContextItem {
uri: vscode.Uri
range?: vscode.Range
Expand Down
59 changes: 20 additions & 39 deletions vscode/src/chat/chat-view/SimpleChatPanelProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,9 @@ export class SimpleChatPanelProvider implements vscode.Disposable, IChatPanelPro
}

private async onInitialized(): Promise<void> {
await this.restoreSession(this.sessionID)
await this.restoreSession(this.sessionID, false)
await this.postChatModels()
await this.saveAndPostHistory()
await this.saveSession()
await this.postCodyCommands()
}

Expand All @@ -393,32 +393,29 @@ export class SimpleChatPanelProvider implements vscode.Disposable, IChatPanelPro
* current in-progress completion. If the chat does not exist, then this
* is a no-op.
*/
public async restoreSession(sessionID: string): Promise<void> {
public async restoreSession(sessionID: string, cancelOngoingCompletion = true): Promise<void> {
const oldTranscript = this.history.getChat(sessionID)
if (!oldTranscript) {
return
}

if (sessionID !== this.sessionID) {
await this.saveSession()
if (cancelOngoingCompletion) {
this.cancelInProgressCompletion()
}
this.cancelInProgressCompletion()
const newModel = await newChatModelfromTranscriptJSON(oldTranscript, this.defaultModelID)
this.chatModel = newModel
this.sessionID = newModel.sessionID

this.postViewTranscript()
}

public async saveSession(): Promise<void> {
if (this.chatModel.isEmpty()) {
return
public async saveSession(humanInput?: string): Promise<void> {
const allHistory = await this.history.saveChat(this.chatModel.toTranscriptJSON(), humanInput)
if (allHistory) {
void this.webview?.postMessage({
type: 'history',
messages: allHistory,
})
}
const allHistory = await this.history.saveChat(this.chatModel.toTranscriptJSON())
void this.webview?.postMessage({
type: 'history',
messages: allHistory,
})
await this.treeView.updateTree(createCodyChatTreeItems())
}

Expand Down Expand Up @@ -487,6 +484,7 @@ export class SimpleChatPanelProvider implements vscode.Disposable, IChatPanelPro
? createDisplayTextWithFileLinks(userContextFiles, text)
: createDisplayTextWithFileSelection(text, this.editor.getActiveTextEditorSelection())
this.chatModel.addHumanMessage({ text }, displayText)
await this.saveSession(text)
// trigger the context progress indicator
this.postViewTranscript({ speaker: 'assistant', text: '' })
await this.generateAssistantResponse(requestID, userContextFiles, addEnhancedContext)
Expand Down Expand Up @@ -707,19 +705,6 @@ export class SimpleChatPanelProvider implements vscode.Disposable, IChatPanelPro
void this.webview?.postMessage({ type: 'errors', errors: error.message })
}

/**
* Send user history to webview, which included chat history and chat input history.
*/
private async saveAndPostHistory(humanInput?: string): Promise<void> {
if (humanInput) {
await this.history.saveHumanInputHistory(humanInput)
}
void this.webview?.postMessage({
type: 'history',
messages: this.history.localHistory,
})
}

/**
* Finalizes adding a bot message to the chat model, with guardrails, and triggers an
* update to the view.
Expand Down Expand Up @@ -801,15 +786,15 @@ export class SimpleChatPanelProvider implements vscode.Disposable, IChatPanelPro
if (!recipeMessages) {
return
}
await this.clearAndRestartSession()
const { humanMessage, prompt } = recipeMessages
const displayText = this.editor.getActiveTextEditorSelection()
? createDisplayTextWithFileSelection(humanChatInput, this.editor.getActiveTextEditorSelection())
: humanChatInput
const { humanMessage, prompt } = recipeMessages
this.chatModel.addHumanMessage(humanMessage.message, displayText)
if (humanMessage.newContextUsed) {
this.chatModel.setNewContextUsed(humanMessage.newContextUsed)
}
await this.saveSession()
this.postViewTranscript()

this.sendLLMRequest(prompt, {
Expand Down Expand Up @@ -921,7 +906,7 @@ class ContextProvider implements IContextProvider {
const searchContext: ContextItem[] = []
let localEmbeddingsError
let remoteEmbeddingsError

searchContext.push(...(await this.getReadmeContext()))
logDebug('SimpleChatPanelProvider', 'getEnhancedContext > embeddings (start)')
const localEmbeddingsResults = this.searchEmbeddingsLocal(text)
const remoteEmbeddingsResults = this.searchEmbeddingsRemote(text)
Expand Down Expand Up @@ -1172,14 +1157,10 @@ class ContextProvider implements IContextProvider {
}

private async getReadmeContext(): Promise<ContextItem[]> {
let readmeUri
const patterns = ['README', 'README.*', 'Readme.*', 'readme.*']
for (const pattern of patterns) {
const files = await vscode.workspace.findFiles(pattern)
if (files.length > 0) {
readmeUri = files[0]
}
}
// global pattern for readme file
const readmeGlobalPattern = '{README,README.,readme.,Readm.}*'
const readmeUri = (await vscode.workspace.findFiles(readmeGlobalPattern, undefined, 1)).at(0)
console.log('Searching for readme file...', readmeUri)
if (!readmeUri) {
return []
}
Expand Down

0 comments on commit 5897d2c

Please sign in to comment.