diff --git a/TESTING.md b/TESTING.md index a930978d53..6be81b5b67 100644 --- a/TESTING.md +++ b/TESTING.md @@ -19,6 +19,9 @@ - [ ] [Read chat history without interruptions](#read-chat-history-without-interruptions) - [ ] [Organize multiple chats](#organize-multiple-chats) - [ ] [Isolate multiple chats](#isolate-multiple-chats) +- Multi-repo context + - [] [Free/pro accounts:](#freepro-accounts) + - [] [Enterprise accounts:](#enterprise-accounts) - Sourcegraph Code Search - [ ] [Find with Sourcegraph...](#find-with-sourcegraph) - [ ] [Search Selection on Sourcegraph Web](#search-selection-on-sourcegraph-web) @@ -52,7 +55,8 @@ Verify the remaining SSO methods by performing the same steps for `Sign in with ### Remove all accounts -Prerequisite: You have to be **signed in**. This is important because we expect certain components to be refreshed automatically. +Prerequisite: You have to be **signed in**. This is important because we expect certain components to be refreshed +automatically. 1. Navigate to `Settings` > `Sourcegraph & Cody`. 2. Remove all accounts and apply settings. @@ -130,9 +134,9 @@ Prerequisite: You have to be **signed in**. This is important because we expect ### General commands availability from keyboard shortcuts -| Command | Windows / Linux | MacOs | -|---------------|------------------------------------------------------|------------------------------------------------------| -| Explain Code | Alt + Shift + 1 | control + Shift + E | +| Command | Windows / Linux | MacOs | +|---------------|--------------------------------------------------|------------------------------------------------------| +| Explain Code | Alt + Shift + 1 | control + Shift + E | | Smell Code | Alt + Shift + 2 | control + Shift + S | | Generate Test | Alt + Shift + 3 | control + Shift + T | @@ -248,21 +252,27 @@ Useful tips: Test ideas: -1. Delete "active" chat. You should be able to delete the currently opened chat. Messages should be removed from Chat tab. +1. Delete "active" chat. You should be able to delete the currently opened chat. Messages should be removed from Chat + tab. 2. Restore historical chat, focus on chat input field and use UP/DOWN keys to cycle between previous questions. 3. Press "new chat" as fast as you can. Especially during the IDE startup. 4. Switch between chats as fast as you can. -5. Press "new chat" while being inside `My Account` tab or something other than Chat tab. Tabs should switch automatically. +5. Press "new chat" while being inside `My Account` tab or something other than Chat tab. Tabs should switch + automatically. 6. Use commands/recipes inside empty, new chat. Verify serialization/deserialization. -7. Ask about codebase to force response with listed context files and verify if everything is correctly serialized/deserialized. Links to context files should be clickable. -8. Remove all chats using history UI. Tree presentation is empty and branches like "Today" are removed from panel. File with transcripts should also disappear. +7. Ask about codebase to force response with listed context files and verify if everything is correctly + serialized/deserialized. Links to context files should be clickable. +8. Remove all chats using history UI. Tree presentation is empty and branches like "Today" are removed from panel. File + with transcripts should also disappear. 9. Use only the keyboard. For example, navigate transcripts with arrows, delete, enter. 10. Start typing while being focused on Chat History to perform search-by-title. 11. Open multiple chats and ask few simultaneous questions in several sessions at once. 12. Open new chat with Alt + = shortcut (or Option + = on Mac). -13. Open existing chat with shortcut Alt + - (or Option + - on Mac) and start typing question. Tab should be switched automatically on Chat. -14. Second click Alt + should hide tool window if focused (similar behavior as other tool windows). -15. Click Esc while being focused inside Cody tool window. You should be automatically focused on code. +13. Open existing chat with shortcut Alt + - (or Option + - on Mac) and + start typing question. Tab should be switched automatically on Chat. +14. Second click Alt + should hide tool window if focused (similar behavior as other tool + windows). +15. Click Esc while being focused inside Cody tool window. You should be automatically focused on code. #### Isolate multiple chats @@ -270,21 +280,58 @@ Prerequisite: You need two working accounts. Preferably one Free, and one Enterp 1. Switch to first account. 2. Send a message. -3. Switch to second account. +3. Switch to second account. 4. Send a message. These two chats should be isolated between different accounts. Both accounts should have one conversation each. You should also be able to switch between accounts while tokens are still being generated. +## Multi-repo context + +### Free/pro accounts: + +1. Open `sourcegraph/cody` project with non-enterprise account. +2. Open new chat and ask question about current repo (e.g. some class) - assistant should know the answer. +3. Open new chat and ask question about squirrel - assistant should describe you an animal. +4. Open new chat and disable local context. Ask about current repo (e.g. some class) - assistant should not have a + context. +5. Save current context as default. Close the IDE. Reopen the IDE. + - Go to Chat History tab and open previous chats one by one. Both history and context settings are properly + preserved. + - Open new chat. Context should be disabled, as we previously set that as new default. Enable it again and set as + default. + +### Enterprise accounts: + +1. Open `sourcegraph/cody` project with enterprise account. +2. Re-do all check from `Testing free/pro accounts` section but now with enterprise account. +3. Click [+] button in the context panel and type sourcegraph repo url (`github.com/sourcegraph/sourcegraph`) + - Validator should block accepting incomplete or invalid URL. + - Add the `sourcegraph/sourcegraph` repo by hitting Add button. +4. Open new chat and ask question about squirrel - assistant should describe you an HTTP server, **NOT** animal. +5. Set current context as default and open new chat. It should use/display default context configuration. +6. Disable `sourcegraph/sourcegraph` remote repo context. +7. Ask question about squirrel. It should again describe you an animal or have no context. +8. Save current context as default. Close the IDE. Reopen the IDE. + - Go to Chat History tab and open previous chats one by one. Check if both history and context settings are properly + preserved. + - Open new chat and check if `sourcegraph/sourcegraph` is disabled. It should be, as we previously set that as a + default. Enable it again and set as default. + - Open new chat and check if `sourcegraph/sourcegraph` is enabled, it should be. + - Remove `sourcegraph/sourcegraph` repo by selecting it, and then clicking [-] button. + - Ask question about squirrel. It should again describe you an animal or have no context. + ## Code Search -All `Code Search` actions are available under the same `Sourcegraph` right-click context menu, so for simplicity, we describe only the **Expected behaviours**. +All `Code Search` actions are available under the same `Sourcegraph` right-click context menu, so for simplicity, we +describe only the **Expected behaviours**. To open the context menu: 1. Open a file in the repository that is indexed by Sourcegraph. -2. Select the fragment of code you want to search for (for example: `System.out.println` or `println` may be the simplest candidate). +2. Select the fragment of code you want to search for (for example: `System.out.println` or `println` may be the + simplest candidate). 3. Right-click on selected fragment, navigate to the `Sourcegraph` sub-menu and choose one of the actions. ### Find with Sourcegraph... @@ -303,7 +350,8 @@ To open the context menu: #### Expected behaviour: 1. The browser is launched. -2. The result is a list of fragments that are found **within the same repository** from which the searched fragment originates. +2. The result is a list of fragments that are found **within the same repository** from which the searched fragment + originates. ### Open Selection on Sourcegraph Web @@ -319,7 +367,8 @@ To open the context menu: 1. A link is copied to the clipboard. 2. Notification pops up with successful message. -3. After pasting the link into the browser, the Code Search page opens with the file and the exact line from which the searched fragment originates. +3. After pasting the link into the browser, the Code Search page opens with the file and the exact line from which the + searched fragment originates. ## [Product-led growth](https://handbook.sourcegraph.com/departments/data-analytics/product-led-growth/) @@ -355,7 +404,8 @@ To open the context menu: 1. Open project with enabled Git VCS. This repository must be publicly available on GitHub. 2. Open to `Cody` tool window. -3. Click on repository button to open `Context Selection` dialog. Button is placed inside `Cody` tool window on left, bottom +3. Click on repository button to open `Context Selection` dialog. Button is placed inside `Cody` tool window on left, + bottom corner. #### Expected behaviour diff --git a/src/main/java/com/sourcegraph/cody/PromptPanel.kt b/src/main/java/com/sourcegraph/cody/PromptPanel.kt index af8689719d..38ee37e97f 100644 --- a/src/main/java/com/sourcegraph/cody/PromptPanel.kt +++ b/src/main/java/com/sourcegraph/cody/PromptPanel.kt @@ -5,6 +5,7 @@ import com.intellij.openapi.actionSystem.CustomShortcutSet import com.intellij.openapi.actionSystem.KeyboardShortcut import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project import com.intellij.ui.DocumentAdapter import com.intellij.ui.components.JBList import com.intellij.ui.components.JBScrollPane @@ -27,9 +28,7 @@ import javax.swing.event.AncestorEvent import javax.swing.event.AncestorListener import javax.swing.event.DocumentEvent -class PromptPanel( - private val chatSession: ChatSession, -) : JLayeredPane() { +class PromptPanel(project: Project, private val chatSession: ChatSession) : JLayeredPane() { /** View components */ private val autoGrowingTextArea = AutoGrowingTextArea(5, 9, this) @@ -45,7 +44,7 @@ class PromptPanel( /** Related components */ private val promptMessageHistory = - CodyChatMessageHistory(CHAT_MESSAGE_HISTORY_CAPACITY, chatSession) + CodyChatMessageHistory(project, CHAT_MESSAGE_HISTORY_CAPACITY, chatSession) init { /** Initialize view */ diff --git a/src/main/java/com/sourcegraph/cody/agent/WebviewPostMessageParams.kt b/src/main/java/com/sourcegraph/cody/agent/WebviewPostMessageParams.kt index 24dcc9fc1b..cb759e2ae3 100644 --- a/src/main/java/com/sourcegraph/cody/agent/WebviewPostMessageParams.kt +++ b/src/main/java/com/sourcegraph/cody/agent/WebviewPostMessageParams.kt @@ -3,8 +3,7 @@ package com.sourcegraph.cody.agent import com.sourcegraph.cody.agent.protocol.ChatError import com.sourcegraph.cody.agent.protocol.ChatMessage import com.sourcegraph.cody.agent.protocol.ContextFile -import kotlinx.serialization.* -import kotlinx.serialization.modules.* +import com.sourcegraph.cody.agent.protocol.Repo /** * A message sent from the webview to the extension host. See vscode/src/chat/protocol.ts for the @@ -18,6 +17,8 @@ data class WebviewMessage( val contextFiles: List? = null, val error: ChatError? = null, val query: String? = null, + val explicitRepos: List? = null, + val repoId: String? = null ) data class WebviewReceiveMessageParams(val id: String, val message: WebviewMessage) diff --git a/src/main/java/com/sourcegraph/vcs/RepoUtil.java b/src/main/java/com/sourcegraph/vcs/RepoUtil.java index b1f71ac6d3..444badc748 100644 --- a/src/main/java/com/sourcegraph/vcs/RepoUtil.java +++ b/src/main/java/com/sourcegraph/vcs/RepoUtil.java @@ -159,7 +159,7 @@ private static String doReplacements( .thenCompose( agent -> agent.getServer().convertGitCloneURLToCodebaseName(new CloneURL(cloneURL))) - .completeOnTimeout(null, 4, TimeUnit.SECONDS) + .completeOnTimeout(null, 15, TimeUnit.SECONDS) .get(); if (codebaseName == null) { diff --git a/src/main/kotlin/com/sourcegraph/cody/CodyToolWindowContent.kt b/src/main/kotlin/com/sourcegraph/cody/CodyToolWindowContent.kt index 40ecac6a9c..76759ef79b 100644 --- a/src/main/kotlin/com/sourcegraph/cody/CodyToolWindowContent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/CodyToolWindowContent.kt @@ -104,7 +104,7 @@ class CodyToolWindowContent(private val project: Project) { } } - @RequiresEdt fun refreshHistoryTree() = historyTree.rebuildTree(project) + @RequiresEdt fun refreshHistoryTree() = historyTree.rebuildTree() @RequiresEdt fun refreshPanelsVisibility() { @@ -144,7 +144,7 @@ class CodyToolWindowContent(private val project: Project) { } private fun removeChat(state: ChatState) { - HistoryService.getInstance().remove(state.internalId) + HistoryService.getInstance(project).remove(state.internalId) if (AgentChatSessionService.getInstance(project).removeSession(state)) { val isVisible = currentChatSession.get()?.getInternalId() == state.internalId if (isVisible) { @@ -155,7 +155,7 @@ class CodyToolWindowContent(private val project: Project) { private fun removeAllChats() { AgentChatSessionService.getInstance(project).removeAllSessions() - HistoryService.getInstance().removeAll() + HistoryService.getInstance(project).removeAll() switchToChatSession(AgentChatSession.createNew(project)) } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentCodebase.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentCodebase.kt index d0212d1c00..a6f2aafc3d 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentCodebase.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentCodebase.kt @@ -8,22 +8,28 @@ import com.intellij.openapi.vfs.VirtualFile import com.sourcegraph.cody.config.CodyProjectSettings import com.sourcegraph.config.ConfigUtil import com.sourcegraph.vcs.RepoUtil +import java.util.concurrent.CompletableFuture @Service(Service.Level.PROJECT) class CodyAgentCodebase(val project: Project) { // TODO: Support list of repository names instead of just one. - private val application = ApplicationManager.getApplication() private val settings = CodyProjectSettings.getInstance(project) - private var inferredUrl: String? = null + private var inferredUrl: CompletableFuture = CompletableFuture() - fun getUrl(): String? = settings.remoteUrl ?: inferredUrl + init { + onFileOpened(project, null) + } + + fun getUrl(): CompletableFuture = + if (settings.remoteUrl != null) CompletableFuture.completedFuture(settings.remoteUrl) + else inferredUrl fun onFileOpened(project: Project, file: VirtualFile?) { - application.executeOnPooledThread { + ApplicationManager.getApplication().executeOnPooledThread { val repositoryName = RepoUtil.findRepositoryName(project, file) - if (repositoryName != null && inferredUrl != repositoryName) { - inferredUrl = repositoryName + if (repositoryName != null && inferredUrl.getNow(null) != repositoryName) { + inferredUrl.complete(repositoryName) CodyAgentService.applyAgentOnBackgroundThread(project) { it.server.configurationDidChange(ConfigUtil.getAgentConfiguration(project)) } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt index 8a4fa21fe4..a42052ce62 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgentServer.kt @@ -1,6 +1,7 @@ package com.sourcegraph.cody.agent import com.sourcegraph.cody.agent.protocol.* +import com.sourcegraph.cody.agent.protocol.util.ChatRemoteReposResponse import java.util.concurrent.CompletableFuture import org.eclipse.lsp4j.jsonrpc.services.JsonNotification import org.eclipse.lsp4j.jsonrpc.services.JsonRequest @@ -35,6 +36,9 @@ interface CodyAgentServer { @JsonRequest("graphql/getRepoId") fun getRepoId(repoName: GetRepoIDResponse): CompletableFuture + @JsonRequest("graphql/getRepoIds") + fun getRepoIds(repoName: GetRepoIdsParam): CompletableFuture + @JsonRequest("git/codebaseName") fun convertGitCloneURLToCodebaseName(cloneURL: CloneURL): CompletableFuture @@ -105,4 +109,6 @@ interface CodyAgentServer { fun attributionSearch( params: AttributionSearchParams ): CompletableFuture + + @JsonRequest("chat/remoteRepos") fun chatRemoteRepos(): CompletableFuture } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/GetRepoIdsParam.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/GetRepoIdsParam.kt new file mode 100644 index 0000000000..7e0ac1a80f --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/GetRepoIdsParam.kt @@ -0,0 +1,3 @@ +package com.sourcegraph.cody.agent.protocol + +data class GetRepoIdsParam(val names: List, val first: Int) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/GetRepoIdsResponse.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/GetRepoIdsResponse.kt new file mode 100644 index 0000000000..edb84c54b2 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/GetRepoIdsResponse.kt @@ -0,0 +1,3 @@ +package com.sourcegraph.cody.agent.protocol + +data class GetRepoIdsResponse(val repos: List) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/Repo.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/Repo.kt new file mode 100644 index 0000000000..c3a0afa97d --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/Repo.kt @@ -0,0 +1,3 @@ +package com.sourcegraph.cody.agent.protocol + +data class Repo(val name: String, val id: String) diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/protocol/util/ChatRemoteReposResponse.kt b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/util/ChatRemoteReposResponse.kt new file mode 100644 index 0000000000..fddc2dc8e5 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/agent/protocol/util/ChatRemoteReposResponse.kt @@ -0,0 +1,5 @@ +package com.sourcegraph.cody.agent.protocol.util + +import com.sourcegraph.cody.agent.protocol.Repo + +data class ChatRemoteReposResponse(val remoteRepos: List) diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/AgentChatSession.kt b/src/main/kotlin/com/sourcegraph/cody/chat/AgentChatSession.kt index 5095103d47..e7eabeaed5 100644 --- a/src/main/kotlin/com/sourcegraph/cody/chat/AgentChatSession.kt +++ b/src/main/kotlin/com/sourcegraph/cody/chat/AgentChatSession.kt @@ -179,7 +179,7 @@ private constructor( } messages.add(message) chatPanel.addOrUpdateMessage(message) - HistoryService.getInstance().update(project, internalId, messages) + HistoryService.getInstance(project).updateChatMessages(internalId, messages) } @RequiresEdt @@ -220,12 +220,7 @@ private constructor( val chatSession = AgentChatSession(project, sessionId) chatSession.createCancellationToken( - onCancel = { - CodyAgentService.applyAgentOnBackgroundThread(project) { agent -> - agent.server.webviewReceiveMessage( - WebviewReceiveMessageParams(sessionId.get(), WebviewMessage(command = "abort"))) - } - }, + onCancel = { chatSession.sendWebviewMessage(WebviewMessage(command = "abort")) }, onFinish = { GraphQlLogger.logCodyEvent(project, "command:${commandId.displayName}", "executed") }) diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/CodyChatMessageHistory.kt b/src/main/kotlin/com/sourcegraph/cody/chat/CodyChatMessageHistory.kt index 6980ff3274..60791d1e69 100644 --- a/src/main/kotlin/com/sourcegraph/cody/chat/CodyChatMessageHistory.kt +++ b/src/main/kotlin/com/sourcegraph/cody/chat/CodyChatMessageHistory.kt @@ -1,11 +1,16 @@ package com.sourcegraph.cody.chat +import com.intellij.openapi.project.Project import com.intellij.ui.components.JBTextArea import com.sourcegraph.cody.history.HistoryService import com.sourcegraph.cody.history.state.MessageState import java.util.* -class CodyChatMessageHistory(private val capacity: Int, chatSession: ChatSession) { +class CodyChatMessageHistory( + private val project: Project, + private val capacity: Int, + chatSession: ChatSession +) { var currentValue: String = "" private var upperStack: Stack = Stack() private var lowerStack: Stack = Stack() @@ -53,8 +58,8 @@ class CodyChatMessageHistory(private val capacity: Int, chatSession: ChatSession } private fun preloadHistoricalMessages(chatSession: ChatSession) { - HistoryService.getInstance() - .state + HistoryService.getInstance(project) + .getHistoryReadOnly() .chats .find { it.internalId == chatSession.getInternalId() } ?.messages diff --git a/src/main/kotlin/com/sourcegraph/cody/chat/ui/ChatPanel.kt b/src/main/kotlin/com/sourcegraph/cody/chat/ui/ChatPanel.kt index a90263c44b..8edb7e633c 100644 --- a/src/main/kotlin/com/sourcegraph/cody/chat/ui/ChatPanel.kt +++ b/src/main/kotlin/com/sourcegraph/cody/chat/ui/ChatPanel.kt @@ -21,10 +21,11 @@ import javax.swing.JPanel class ChatPanel(project: Project, chatSession: ChatSession) : JPanel(VerticalFlowLayout(VerticalFlowLayout.CENTER, 0, 0, true, false)) { - val promptPanel: PromptPanel = PromptPanel(chatSession) + val promptPanel: PromptPanel = PromptPanel(project, chatSession) private val messagesPanel = MessagesPanel(project, chatSession) private val chatPanel = ChatScrollPane(messagesPanel) - private val contextView: EnhancedContextPanel = EnhancedContextPanel(project) + + private val contextView: EnhancedContextPanel = EnhancedContextPanel(project, chatSession) private val stopGeneratingButton = object : JButton("Stop generating", IconUtil.desaturate(AllIcons.Actions.Suspend)) { diff --git a/src/main/kotlin/com/sourcegraph/cody/config/CodyAccountDetailsProvider.kt b/src/main/kotlin/com/sourcegraph/cody/config/CodyAccountDetailsProvider.kt index 0a7ff83753..c46de17618 100644 --- a/src/main/kotlin/com/sourcegraph/cody/config/CodyAccountDetailsProvider.kt +++ b/src/main/kotlin/com/sourcegraph/cody/config/CodyAccountDetailsProvider.kt @@ -22,12 +22,15 @@ class CodyAccountDetailsProvider( account: CodyAccount, indicator: ProgressIndicator ): CompletableFuture> { - val token = - accountsModel.newCredentials.getOrElse(account) { accountManager.findCredentials(account) } - ?: return CompletableFuture.completedFuture(noToken()) - val executor = service().create(token) + return ProgressManager.getInstance() .submitIOTask(indicator) { indicator -> + val token = + accountsModel.newCredentials.getOrElse(account) { + accountManager.findCredentials(account) + } ?: return@submitIOTask noToken() + val executor = service().create(token) + if (account.isCodyApp()) { val details = CodyAccountDetails(account.id, account.name, account.name, null) DetailsLoadingResult(details, IconUtil.toBufferedImage(defaultIcon), null, false) diff --git a/src/main/kotlin/com/sourcegraph/cody/config/DialogValidationUtils.kt b/src/main/kotlin/com/sourcegraph/cody/config/DialogValidationUtils.kt index 6053c3f4db..9943fc5944 100644 --- a/src/main/kotlin/com/sourcegraph/cody/config/DialogValidationUtils.kt +++ b/src/main/kotlin/com/sourcegraph/cody/config/DialogValidationUtils.kt @@ -2,6 +2,7 @@ package com.sourcegraph.cody.config import com.intellij.openapi.ui.ValidationInfo import com.intellij.openapi.util.NlsContexts +import javax.swing.JComponent import javax.swing.JTextField object DialogValidationUtils { @@ -12,11 +13,11 @@ object DialogValidationUtils { /** Returns [ValidationInfo] with [message] if [isValid] returns false */ fun custom( - textField: JTextField, + component: JComponent, @NlsContexts.DialogMessage message: String, isValid: () -> Boolean ): ValidationInfo? { - return if (!isValid()) ValidationInfo(message, textField) else null + return if (!isValid()) ValidationInfo(message, component) else null } /** diff --git a/src/main/kotlin/com/sourcegraph/cody/config/SettingsMigration.kt b/src/main/kotlin/com/sourcegraph/cody/config/SettingsMigration.kt index adeb8a3ca4..e65747be54 100644 --- a/src/main/kotlin/com/sourcegraph/cody/config/SettingsMigration.kt +++ b/src/main/kotlin/com/sourcegraph/cody/config/SettingsMigration.kt @@ -41,7 +41,7 @@ class SettingsMigration : Activity { private fun migrateOrphanedChatsToActiveAccount(project: Project) { val activeAccountId = CodyAuthenticationManager.instance.getActiveAccount(project)?.id - HistoryService.getInstance() + HistoryService.getInstance(project) .state .chats .filter { it.accountId == null } diff --git a/src/main/kotlin/com/sourcegraph/cody/context/RemoteRepoUtils.kt b/src/main/kotlin/com/sourcegraph/cody/context/RemoteRepoUtils.kt new file mode 100644 index 0000000000..4f5bc20fdb --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/context/RemoteRepoUtils.kt @@ -0,0 +1,23 @@ +package com.sourcegraph.cody.context + +import com.intellij.openapi.project.Project +import com.sourcegraph.cody.agent.CodyAgentService +import com.sourcegraph.cody.agent.protocol.GetRepoIdsParam +import com.sourcegraph.cody.agent.protocol.Repo +import java.util.concurrent.CompletableFuture + +object RemoteRepoUtils { + fun getRepository(project: Project, url: String): CompletableFuture { + val result = CompletableFuture>() + CodyAgentService.applyAgentOnBackgroundThread(project) { agent -> + try { + agent.server.getRepoIds(GetRepoIdsParam(listOf(url), 1)).thenApply { + result.complete(it?.repos) + } + } catch (e: Exception) { + result.complete(null) + } + } + return result.thenApply { it?.firstOrNull() } + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/context/ui/AddRepositoryDialog.kt b/src/main/kotlin/com/sourcegraph/cody/context/ui/AddRepositoryDialog.kt new file mode 100644 index 0000000000..1334ff6b1c --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/context/ui/AddRepositoryDialog.kt @@ -0,0 +1,94 @@ +package com.sourcegraph.cody.context.ui + +import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.TextFieldWithAutoCompletion +import com.intellij.ui.TextFieldWithAutoCompletionListProvider +import com.intellij.util.Alarm +import com.sourcegraph.cody.config.DialogValidationUtils +import com.sourcegraph.cody.context.RemoteRepoUtils +import java.net.URL +import java.util.concurrent.TimeUnit +import javax.swing.JComponent +import javax.swing.JLabel +import javax.swing.JPanel +import org.jetbrains.annotations.NotNull + +class AddRepositoryDialog(private val project: Project, private val addAction: (String) -> Unit) : + DialogWrapper(project) { + + private val repoUrlInputField = TextFieldWithAutoCompletion.create(project, listOf(), false, null) + + init { + init() + title = "Add Remote Repository" + setOKButtonText("Add") + setValidationDelay(100) + } + + override fun doValidateAll(): List { + fun validateNonEmpty() = + DialogValidationUtils.custom(repoUrlInputField, "Remote repository URL cannot be empty") { + repoUrlInputField.text.isNotBlank() + } + + fun validateValidUrl() = + DialogValidationUtils.custom(repoUrlInputField, "Remote repository URL must be valid") { + val url = + if (repoUrlInputField.text.startsWith("http")) repoUrlInputField.text + else "http://" + repoUrlInputField.text + runCatching { URL(url) }.isSuccess + } + + fun validateRepoExists() = + DialogValidationUtils.custom( + repoUrlInputField, "Remote repository not found on the server") { + val repo = + RemoteRepoUtils.getRepository(project, repoUrlInputField.text) + .completeOnTimeout(null, 2, TimeUnit.SECONDS) + .get() + repo != null + } + + return listOfNotNull(validateNonEmpty() ?: validateValidUrl() ?: validateRepoExists()) + } + + override fun getValidationThreadToUse(): Alarm.ThreadToUse { + return Alarm.ThreadToUse.POOLED_THREAD + } + + override fun doOKAction() { + addAction(repoUrlInputField.text) + close(OK_EXIT_CODE, true) + } + + override fun createCenterPanel(): JComponent { + val panel = JPanel() + val label = JLabel("Repository URL: ") + panel.add(label) + + // TODO: we can provide repository suggestions using `provider.setItems` method + val completionProvider: TextFieldWithAutoCompletionListProvider = + object : TextFieldWithAutoCompletionListProvider(listOf()) { + @NotNull + override fun getLookupString(@NotNull s: String): String { + return s + } + } + + repoUrlInputField.setPreferredWidth(300) + repoUrlInputField.installProvider(completionProvider) + repoUrlInputField.addDocumentListener( + object : DocumentListener { + override fun documentChanged(event: com.intellij.openapi.editor.event.DocumentEvent) { + initValidation() + } + }) + + panel.add((repoUrlInputField)) + + return panel + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextRepositoriesCheckboxRenderer.kt b/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextRepositoriesCheckboxRenderer.kt index a12d55486e..81ee6f32b4 100644 --- a/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextRepositoriesCheckboxRenderer.kt +++ b/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextRepositoriesCheckboxRenderer.kt @@ -1,34 +1,43 @@ package com.sourcegraph.cody.context.ui -import com.intellij.openapi.project.Project +import com.intellij.openapi.application.ApplicationInfo import com.intellij.ui.CheckboxTree import com.intellij.ui.CheckedTreeNode import com.intellij.ui.SimpleTextAttributes +import java.io.File import javax.swing.JTree class ContextRepositoriesCheckboxRenderer : CheckboxTree.CheckboxTreeCellRenderer() { override fun customizeRenderer( tree: JTree?, - value: Any?, + node: Any?, selected: Boolean, expanded: Boolean, leaf: Boolean, row: Int, hasFocus: Boolean ) { - when (value) { + val style = + if (ApplicationInfo.getInstance().build.baselineVersion > 233) "style='color:#808080'" + else "" + + when (node) { + is ContextTreeRemoteRepoNode -> { + val repoName = node.repoUrl.split(File.separator).lastOrNull() ?: node.repoUrl + textRenderer.appendHTML( + "${repoName} ${node.repoUrl}", + SimpleTextAttributes.REGULAR_ATTRIBUTES) + } + is ContextTreeLocalRepoNode -> { + val projectPath = node.project.basePath?.replace(System.getProperty("user.home"), "~") + textRenderer.appendHTML( + "${node.project.name} ${projectPath}", + SimpleTextAttributes.REGULAR_ATTRIBUTES) + } is CheckedTreeNode -> { - when (val userObject = value.userObject) { - is Project -> { - textRenderer.appendHTML( - "${userObject.name} - ${userObject.basePath}", - SimpleTextAttributes.REGULAR_ATTRIBUTES) - } - is String -> { - textRenderer.appendHTML(userObject, SimpleTextAttributes.REGULAR_ATTRIBUTES) - } - } + textRenderer.appendHTML( + "${node.userObject}", SimpleTextAttributes.REGULAR_ATTRIBUTES) } } } diff --git a/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextToolbarButton.kt b/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextToolbarButton.kt new file mode 100644 index 0000000000..bd5e63fcda --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextToolbarButton.kt @@ -0,0 +1,17 @@ +package com.sourcegraph.cody.context.ui + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.ui.ToolbarDecorator +import javax.swing.Icon + +open class ContextToolbarButton( + name: String, + icon: Icon, + private val buttonAction: () -> Unit = {} +) : ToolbarDecorator.ElementActionButton(name, icon) { + override fun isDumbAware(): Boolean = true + + override fun actionPerformed(p0: AnActionEvent) { + buttonAction() + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextTreeNode.kt b/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextTreeNode.kt new file mode 100644 index 0000000000..70e9aa933d --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/context/ui/ContextTreeNode.kt @@ -0,0 +1,38 @@ +package com.sourcegraph.cody.context.ui + +import com.intellij.openapi.project.Project +import com.intellij.ui.CheckedTreeNode + +open class ContextTreeNode(value: T, private val onSetChecked: (Boolean) -> Unit) : + CheckedTreeNode(value) { + override fun setChecked(checked: Boolean) { + super.setChecked(checked) + onSetChecked(checked) + } +} + +class ContextTreeRootNode( + val text: String, + isEnabled: Boolean = true, + onSetChecked: (Boolean) -> Unit = {} +) : ContextTreeNode(text, onSetChecked) { + init { + this.isEnabled = isEnabled + } +} + +class ContextTreeRemoteRepoNode( + val repoUrl: String, + isChecked: Boolean, + onSetChecked: (Boolean) -> Unit +) : ContextTreeNode(repoUrl, onSetChecked) { + init { + this.isChecked = isChecked + } +} + +class ContextTreeLocalRepoNode(val project: Project) : ContextTreeNode(project, {}) { + init { + this.isEnabled = false + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/context/ui/EnhancedContextPanel.kt b/src/main/kotlin/com/sourcegraph/cody/context/ui/EnhancedContextPanel.kt index 0b971c3104..dff2b398e7 100644 --- a/src/main/kotlin/com/sourcegraph/cody/context/ui/EnhancedContextPanel.kt +++ b/src/main/kotlin/com/sourcegraph/cody/context/ui/EnhancedContextPanel.kt @@ -1,58 +1,260 @@ package com.sourcegraph.cody.context.ui -import com.intellij.openapi.actionSystem.ActionToolbarPosition +import com.intellij.icons.AllIcons +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project import com.intellij.openapi.ui.VerticalFlowLayout import com.intellij.ui.CheckboxTree +import com.intellij.ui.CheckboxTreeBase import com.intellij.ui.CheckedTreeNode -import com.intellij.ui.ToolbarDecorator -import com.sourcegraph.cody.agent.CodyAgentService +import com.intellij.ui.ToolbarDecorator.createDecorator +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.sourcegraph.cody.agent.CodyAgentCodebase +import com.sourcegraph.cody.agent.WebviewMessage +import com.sourcegraph.cody.agent.protocol.Repo +import com.sourcegraph.cody.chat.ChatSession +import com.sourcegraph.cody.config.CodyAuthenticationManager +import com.sourcegraph.cody.context.RemoteRepoUtils +import com.sourcegraph.cody.history.HistoryService +import com.sourcegraph.cody.history.state.EnhancedContextState +import com.sourcegraph.cody.history.state.RemoteRepositoryState import com.sourcegraph.common.CodyBundle import java.awt.Dimension import java.util.concurrent.atomic.AtomicBoolean +import java.util.function.Consumer import javax.swing.BorderFactory import javax.swing.JPanel +import javax.swing.event.TreeExpansionEvent +import javax.swing.event.TreeExpansionListener +import javax.swing.tree.DefaultTreeModel -class EnhancedContextPanel(private val project: Project) : JPanel() { +class EnhancedContextPanel(private val project: Project, private val chatSession: ChatSession) : + JPanel() { val isEnhancedContextEnabled = AtomicBoolean(true) - private val reindexButton = ReindexButton(project) + private val treeRoot = CheckedTreeNode(CodyBundle.getString("context-panel.tree.root")) + private val enhancedContextNode = + ContextTreeRootNode(CodyBundle.getString("context-panel.tree.node-chat-context")) { isChecked + -> + isEnhancedContextEnabled.set(isChecked) + updateContextState { it.isEnabled = isChecked } + } + private val localContextNode = + ContextTreeRootNode( + CodyBundle.getString("context-panel.tree.node-local-project"), isEnabled = false) + private val remoteContextNode = + ContextTreeRootNode(CodyBundle.getString("context-panel.tree.node-remote-repos")) + private val localProjectNode = ContextTreeLocalRepoNode(project) - private val helpButton = HelpButton() + private val treeModel = DefaultTreeModel(treeRoot) private val tree = run { - val treeRoot = CheckedTreeNode(CodyBundle.getString("context-panel.tree-root")) - val chatContext = CheckedTreeNode(CodyBundle.getString("context-panel.tree-chat-context")) - val currentRepo = - object : CheckedTreeNode(project) { - override fun isChecked(): Boolean = isEnhancedContextEnabled.get() - - override fun setChecked(checked: Boolean) { - isEnhancedContextEnabled.set(checked) - CodyAgentService.getInstance(project).restartAgent(project) - } + val checkboxPropagationPolicy = + CheckboxTreeBase.CheckPolicy( + /* checkChildrenWithCheckedParent = */ true, + /* uncheckChildrenWithUncheckedParent = */ true, + /* checkParentWithCheckedChild = */ false, + /* uncheckParentWithUncheckedChild = */ false) + CheckboxTree(ContextRepositoriesCheckboxRenderer(), treeRoot, checkboxPropagationPolicy) + } + + @RequiresEdt + private fun prepareTree() { + treeRoot.add(enhancedContextNode) + localContextNode.add(localProjectNode) + enhancedContextNode.add(localContextNode) + + val contextState = getContextState() + + ApplicationManager.getApplication().invokeLater { + enhancedContextNode.isChecked = contextState.isEnabled + localContextNode.isChecked = contextState.isEnabled + localProjectNode.isChecked = contextState.isEnabled + } + + if (!isDotComAccount()) { + enhancedContextNode.add(remoteContextNode) + if (contextState.remoteRepositories.isNotEmpty()) { + contextState.remoteRepositories.forEach { repo -> + repo.remoteUrl?.let { remoteUrl -> addRemoteRepository(remoteUrl, repo.isEnabled) } + } + } else { + CodyAgentCodebase.getInstance(project).getUrl().thenApply { repo -> + addRemoteRepository(repo) + } + } + } + + treeModel.reload() + } + + private fun getContextState(): EnhancedContextState { + val historyService = HistoryService.getInstance(project) + + return historyService.getOrCreateChatReadOnly(chatSession.getInternalId()).enhancedContext + ?: historyService.getHistoryReadOnly().defaultEnhancedContext + ?: EnhancedContextState() + } + + private fun updateContextState(consumer: Consumer) { + val contextState = getContextState() + consumer.accept(contextState) + HistoryService.getInstance(project) + .updateContextState(chatSession.getInternalId(), contextState) + } + + private fun isDotComAccount() = + CodyAuthenticationManager.instance.getActiveAccount(project)?.isDotcomAccount() ?: false + + private fun getRepoByUrlAndRun(repoUrl: String, consumer: Consumer) { + RemoteRepoUtils.getRepository(project, repoUrl).thenApply { + it?.let { repo -> consumer.accept(repo) } + } + } + + private fun enableRemote(repoUrl: String) { + updateContextState { contextState -> + contextState.remoteRepositories.find { it.remoteUrl == repoUrl }?.isEnabled = true + } + getRepoByUrlAndRun(repoUrl) { repo -> + chatSession.sendWebviewMessage( + WebviewMessage( + command = "context/choose-remote-search-repo", explicitRepos = listOf(repo))) + } + } + + private fun disableRemote(repoUrl: String) { + updateContextState { contextState -> + contextState.remoteRepositories.find { it.remoteUrl == repoUrl }?.isEnabled = false + } + getRepoByUrlAndRun(repoUrl) { repo -> + chatSession.sendWebviewMessage( + WebviewMessage(command = "context/remove-remote-search-repo", repoId = repo.id)) + } + } + + @RequiresEdt + private fun removeRemoteRepository(node: ContextTreeRemoteRepoNode) { + updateContextState { contextState -> + contextState.remoteRepositories.removeIf { it.remoteUrl == node.repoUrl } + } + remoteContextNode.remove(node) + treeModel.reload() + disableRemote(node.repoUrl) + } + + @RequiresEdt + private fun addRemoteRepository(repoUrl: String, isCheckedInitially: Boolean = true) { + val existingRemote = getContextState().remoteRepositories.find { it.remoteUrl == repoUrl } + if (existingRemote == null) { + updateContextState { contextState -> + contextState.remoteRepositories.add( + RemoteRepositoryState.create(repoUrl, isCheckedInitially)) + } + } else { + existingRemote.isEnabled = isCheckedInitially + existingRemote.remoteUrl = repoUrl + } + + val remoteRepoNode = + ContextTreeRemoteRepoNode(repoUrl, isChecked = isCheckedInitially) { isChecked -> + if (isChecked) enableRemote(repoUrl) else disableRemote(repoUrl) } - chatContext.add(currentRepo) - treeRoot.add(chatContext) - CheckboxTree(ContextRepositoriesCheckboxRenderer(), treeRoot) - } - - private val toolbarPanel = - ToolbarDecorator.createDecorator(tree) - .disableUpDownActions() - .addExtraAction(reindexButton) - .addExtraAction(helpButton) - .setPreferredSize(Dimension(0, 30)) - .setToolbarPosition(ActionToolbarPosition.LEFT) - .setScrollPaneBorder(BorderFactory.createEmptyBorder()) - .setToolbarBorder(BorderFactory.createEmptyBorder()) - .createPanel() + + remoteContextNode.add(remoteRepoNode) + treeModel.reload() + } + + @RequiresEdt + private fun expandAllNodes(rowCount: Int = tree.rowCount) { + for (i in 0 until tree.rowCount) { + tree.expandRow(i) + } + + if (tree.getRowCount() != rowCount) { + expandAllNodes(tree.rowCount) + } + } init { - layout = VerticalFlowLayout(VerticalFlowLayout.BOTTOM, 0, 5, true, false) - tree.expandRow(0) + layout = VerticalFlowLayout(VerticalFlowLayout.BOTTOM, 0, 0, true, false) + tree.setModel(treeModel) + prepareTree() + + val toolbarDecorator = + createDecorator(tree) + .disableUpDownActions() + .setVisibleRowCount(1) + .setScrollPaneBorder(BorderFactory.createEmptyBorder()) + .setToolbarBorder(BorderFactory.createEmptyBorder()) + + if (!isDotComAccount()) { + toolbarDecorator.setAddActionName( + CodyBundle.getString("context-panel.button.add-remote-repo")) + toolbarDecorator.setAddAction { + AddRepositoryDialog(project) { repoUrl -> addRemoteRepository(repoUrl) }.show() + expandAllNodes() + } + + toolbarDecorator.setRemoveActionName( + CodyBundle.getString("context-panel.button.remove-remote-repo")) + toolbarDecorator.setRemoveActionUpdater { + tree.selectionPath?.lastPathComponent is ContextTreeRemoteRepoNode + } + toolbarDecorator.setRemoveAction { + (tree.selectionPath?.lastPathComponent as? ContextTreeRemoteRepoNode)?.let { node -> + removeRemoteRepository(node) + expandAllNodes() + } + } + } + + toolbarDecorator.addExtraAction(ReindexButton(project)) + + toolbarDecorator.addExtraAction( + ContextToolbarButton( + CodyBundle.getString("context-panel.button.save-default"), + AllIcons.Actions.SetDefault) { + HistoryService.getInstance(project).updateDefaultContextState(getContextState()) + }) + + toolbarDecorator.addExtraAction( + ContextToolbarButton( + CodyBundle.getString("context-panel.button.restore-default"), AllIcons.General.Reset) { + HistoryService.getInstance(project) + .updateContextState(chatSession.getInternalId(), contextState = null) + treeRoot.removeAllChildren() + prepareTree() + }) + + toolbarDecorator.addExtraAction(HelpButton()) + + val panel = toolbarDecorator.createPanel() + + tree.addTreeExpansionListener( + object : TreeExpansionListener { + private fun resize() { + val padding = 5 + val actionsPanelHeight = toolbarDecorator.actionsPanel.height + panel.preferredSize = + Dimension(0, padding + actionsPanelHeight + tree.rowCount * tree.rowHeight) + panel.parent.revalidate() + } + + override fun treeExpanded(event: TreeExpansionEvent) { + val component = event.path.lastPathComponent + if (component is ContextTreeRootNode && component == enhancedContextNode) { + expandAllNodes() + } + resize() + } + + override fun treeCollapsed(event: TreeExpansionEvent) { + resize() + } + }) - add(toolbarPanel) + add(panel) } } diff --git a/src/main/kotlin/com/sourcegraph/cody/context/ui/HelpButton.kt b/src/main/kotlin/com/sourcegraph/cody/context/ui/HelpButton.kt index 6892ff1554..e376628b1a 100644 --- a/src/main/kotlin/com/sourcegraph/cody/context/ui/HelpButton.kt +++ b/src/main/kotlin/com/sourcegraph/cody/context/ui/HelpButton.kt @@ -2,16 +2,10 @@ package com.sourcegraph.cody.context.ui import com.intellij.icons.AllIcons import com.intellij.ide.BrowserUtil -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.ui.ToolbarDecorator import com.sourcegraph.common.CodyBundle class HelpButton : - ToolbarDecorator.ElementActionButton( - CodyBundle.getString("context-panel.help-button-name"), AllIcons.Actions.Help) { - override fun actionPerformed(p0: AnActionEvent) { - BrowserUtil.open("https://docs.sourcegraph.com/cody/core-concepts/keyword-search") - } - - override fun isDumbAware(): Boolean = true -} + ContextToolbarButton( + CodyBundle.getString("context-panel.button.help"), + AllIcons.Actions.Help, + { BrowserUtil.open("https://sourcegraph.com/docs/cody/core-concepts/context") }) diff --git a/src/main/kotlin/com/sourcegraph/cody/context/ui/ReindexButton.kt b/src/main/kotlin/com/sourcegraph/cody/context/ui/ReindexButton.kt index 4b851354e2..72d750d77f 100644 --- a/src/main/kotlin/com/sourcegraph/cody/context/ui/ReindexButton.kt +++ b/src/main/kotlin/com/sourcegraph/cody/context/ui/ReindexButton.kt @@ -7,7 +7,6 @@ import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages -import com.intellij.ui.ToolbarDecorator import com.sourcegraph.cody.agent.CodyAgentService import com.sourcegraph.cody.agent.CommandExecuteParams import com.sourcegraph.common.CodyBundle @@ -15,8 +14,8 @@ import com.sourcegraph.common.CodyBundle.fmt import java.util.concurrent.atomic.AtomicBoolean class ReindexButton(private val project: Project) : - ToolbarDecorator.ElementActionButton( - CodyBundle.getString("context-panel.reindex-button-name"), AllIcons.Actions.Refresh) { + ContextToolbarButton( + CodyBundle.getString("context-panel.button.reindex"), AllIcons.Actions.Refresh) { override fun actionPerformed(p0: AnActionEvent) { CodyAgentService.applyAgentOnBackgroundThread(project) { agent -> ProgressManager.getInstance() @@ -45,7 +44,5 @@ class ReindexButton(private val project: Project) : override fun isEnabled(): Boolean = !isReindexingInProgress.get() - override fun isDumbAware(): Boolean = true - private val isReindexingInProgress = AtomicBoolean(false) } diff --git a/src/main/kotlin/com/sourcegraph/cody/history/HistoryService.kt b/src/main/kotlin/com/sourcegraph/cody/history/HistoryService.kt index 5b3b80f72a..0479cb474e 100644 --- a/src/main/kotlin/com/sourcegraph/cody/history/HistoryService.kt +++ b/src/main/kotlin/com/sourcegraph/cody/history/HistoryService.kt @@ -2,17 +2,20 @@ package com.sourcegraph.cody.history import com.intellij.openapi.components.* import com.intellij.openapi.project.Project +import com.jetbrains.rd.framework.base.deepClonePolymorphic import com.sourcegraph.cody.agent.protocol.ChatMessage import com.sourcegraph.cody.agent.protocol.Speaker import com.sourcegraph.cody.config.CodyAuthenticationManager import com.sourcegraph.cody.history.state.ChatState +import com.sourcegraph.cody.history.state.EnhancedContextState import com.sourcegraph.cody.history.state.HistoryState import com.sourcegraph.cody.history.state.MessageState import java.time.LocalDateTime @State(name = "ChatHistory", storages = [Storage("cody_history.xml")]) @Service(Service.Level.PROJECT) -class HistoryService : SimplePersistentStateComponent(HistoryState()) { +class HistoryService(private val project: Project) : + SimplePersistentStateComponent(HistoryState()) { private val listeners = mutableListOf<(ChatState) -> Unit>() @@ -20,8 +23,9 @@ class HistoryService : SimplePersistentStateComponent(HistoryState synchronized(listeners) { listeners += listener } } - fun update(project: Project, internalId: String, chatMessages: List) { - val found = getChatOrCreate(project, internalId) + @Synchronized + fun updateChatMessages(internalId: String, chatMessages: List) { + val found = getOrCreateChat(internalId) found.messages = chatMessages.map(::convertToMessageState).toMutableList() if (chatMessages.lastOrNull()?.speaker == Speaker.HUMAN) { found.setUpdatedTimeAt(LocalDateTime.now()) @@ -29,6 +33,28 @@ class HistoryService : SimplePersistentStateComponent(HistoryState synchronized(listeners) { listeners.forEach { it(found) } } } + @Synchronized + fun updateContextState(internalId: String, contextState: EnhancedContextState?) { + val found = getOrCreateChat(internalId) + found.enhancedContext = contextState + } + + @Synchronized + fun updateDefaultContextState(contextState: EnhancedContextState) { + state.defaultEnhancedContext = contextState + } + + @Synchronized + fun getOrCreateChatReadOnly(internalId: String): ChatState { + return getOrCreateChat(internalId).deepClonePolymorphic() + } + + @Synchronized + fun getHistoryReadOnly(): HistoryState { + return state.deepClonePolymorphic() + } + + @Synchronized fun remove(internalId: String?) { state.chats.removeIf { it.internalId == internalId } } @@ -48,7 +74,7 @@ class HistoryService : SimplePersistentStateComponent(HistoryState return message } - private fun getChatOrCreate(project: Project, internalId: String): ChatState { + private fun getOrCreateChat(internalId: String): ChatState { val found = state.chats.find { it.internalId == internalId } if (found != null) return found val activeAccountId = CodyAuthenticationManager.instance.getActiveAccount(project)?.id @@ -58,7 +84,6 @@ class HistoryService : SimplePersistentStateComponent(HistoryState } companion object { - - @JvmStatic fun getInstance() = service() + @JvmStatic fun getInstance(project: Project): HistoryService = project.service() } } diff --git a/src/main/kotlin/com/sourcegraph/cody/history/HistoryTree.kt b/src/main/kotlin/com/sourcegraph/cody/history/HistoryTree.kt index bd54a7ecef..bcddaf4efa 100644 --- a/src/main/kotlin/com/sourcegraph/cody/history/HistoryTree.kt +++ b/src/main/kotlin/com/sourcegraph/cody/history/HistoryTree.kt @@ -29,13 +29,13 @@ import javax.swing.tree.TreePath import javax.swing.tree.TreeSelectionModel class HistoryTree( - project: Project, + private val project: Project, private val onSelect: (ChatState) -> Unit, private val onRemove: (ChatState) -> Unit, private val onRemoveAll: () -> Unit ) : SimpleToolWindowPanel(true, true) { - private val model = DefaultTreeModel(buildTree(project)) + private val model = DefaultTreeModel(buildTree()) private val root get() = model.root as RootNode @@ -72,11 +72,11 @@ class HistoryTree( PopupHandler.installPopupMenu(tree, group, "ChatActionsPopup") EditSourceOnDoubleClickHandler.install(tree, ::selectLeaf) setContent(ScrollPaneFactory.createScrollPane(tree)) - HistoryService.getInstance().listenOnUpdate(::updatePresentation) + HistoryService.getInstance(project).listenOnUpdate(::updatePresentation) } - fun rebuildTree(project: Project) { - model.setRoot(buildTree(project)) + fun rebuildTree() { + model.setRoot(buildTree()) } private fun updatePresentation(chat: ChatState) { @@ -164,9 +164,9 @@ class HistoryTree( model.setRoot(RootNode()) } - private fun buildTree(project: Project): DefaultMutableTreeNode { + private fun buildTree(): DefaultMutableTreeNode { val root = RootNode() - for ((period, chats) in getChatsGroupedByPeriod(project)) { + for ((period, chats) in getChatsGroupedByPeriod()) { val periodNode = PeriodNode(period) for (chat in chats) { periodNode.add(LeafNode(chat)) @@ -176,13 +176,14 @@ class HistoryTree( return root } - private fun getChatsGroupedByPeriod(project: Project): Map> = - HistoryService.getInstance() + private fun getChatsGroupedByPeriod(): Map> = + HistoryService.getInstance(project) .state .chats .filter { it.accountId == CodyAuthenticationManager.instance.getActiveAccount(project)?.id } + .filter { it.messages.isNotEmpty() } .sortedByDescending { chat -> chat.getUpdatedTimeAt() } .groupBy { chat -> DurationGroupFormatter.format(chat.getUpdatedTimeAt()) } diff --git a/src/main/kotlin/com/sourcegraph/cody/history/state/ChatState.kt b/src/main/kotlin/com/sourcegraph/cody/history/state/ChatState.kt index d25fe60512..376d096469 100644 --- a/src/main/kotlin/com/sourcegraph/cody/history/state/ChatState.kt +++ b/src/main/kotlin/com/sourcegraph/cody/history/state/ChatState.kt @@ -18,6 +18,9 @@ class ChatState : BaseState() { @get:OptionTag(tag = "accountId", nameAttribute = "") var accountId: String? by string() + @get:OptionTag(tag = "enhancedContext", nameAttribute = "") + var enhancedContext: EnhancedContextState? by property() + fun title(): String? = messages.firstOrNull()?.text fun setUpdatedTimeAt(date: LocalDateTime) { diff --git a/src/main/kotlin/com/sourcegraph/cody/history/state/EnhancedContextState.kt b/src/main/kotlin/com/sourcegraph/cody/history/state/EnhancedContextState.kt new file mode 100644 index 0000000000..ab06679c2f --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/history/state/EnhancedContextState.kt @@ -0,0 +1,13 @@ +package com.sourcegraph.cody.history.state + +import com.intellij.openapi.components.BaseState +import com.intellij.util.xmlb.annotations.OptionTag +import com.intellij.util.xmlb.annotations.Tag + +@Tag("enhancedContext") +class EnhancedContextState : BaseState() { + @get:OptionTag(tag = "isEnabled", nameAttribute = "") var isEnabled: Boolean by property(true) + + @get:OptionTag(tag = "remoteRepositories", nameAttribute = "") + var remoteRepositories: MutableList by list() +} diff --git a/src/main/kotlin/com/sourcegraph/cody/history/state/HistoryState.kt b/src/main/kotlin/com/sourcegraph/cody/history/state/HistoryState.kt index b65aea51f4..2726e887b4 100644 --- a/src/main/kotlin/com/sourcegraph/cody/history/state/HistoryState.kt +++ b/src/main/kotlin/com/sourcegraph/cody/history/state/HistoryState.kt @@ -6,4 +6,7 @@ import com.intellij.util.xmlb.annotations.OptionTag class HistoryState : BaseState() { @get:OptionTag(tag = "chats", nameAttribute = "") var chats: MutableList by list() + + @get:OptionTag(tag = "defaultEnhancedContext", nameAttribute = "") + var defaultEnhancedContext: EnhancedContextState? by property() } diff --git a/src/main/kotlin/com/sourcegraph/cody/history/state/RemoteRepositoryState.kt b/src/main/kotlin/com/sourcegraph/cody/history/state/RemoteRepositoryState.kt new file mode 100644 index 0000000000..5fdb13d297 --- /dev/null +++ b/src/main/kotlin/com/sourcegraph/cody/history/state/RemoteRepositoryState.kt @@ -0,0 +1,22 @@ +package com.sourcegraph.cody.history.state + +import com.intellij.openapi.components.BaseState +import com.intellij.util.xmlb.annotations.OptionTag +import com.intellij.util.xmlb.annotations.Tag + +@Tag("remoteRepository") +class RemoteRepositoryState : BaseState() { + + @get:OptionTag(tag = "isEnabled", nameAttribute = "") var isEnabled: Boolean by property(true) + + @get:OptionTag(tag = "remoteUrl", nameAttribute = "") var remoteUrl: String? by string() + + companion object { + fun create(remoteUrl: String, isEnabled: Boolean): RemoteRepositoryState { + val state = RemoteRepositoryState() + state.isEnabled = isEnabled + state.remoteUrl = remoteUrl + return state + } + } +} diff --git a/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt b/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt index 93e59457ac..1b671edc53 100644 --- a/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt +++ b/src/main/kotlin/com/sourcegraph/cody/initialization/PostStartupActivity.kt @@ -3,7 +3,6 @@ package com.sourcegraph.cody.initialization import com.intellij.openapi.project.Project import com.intellij.openapi.startup.StartupActivity import com.sourcegraph.cody.CodyFocusChangeListener -import com.sourcegraph.cody.agent.CodyAgentCodebase import com.sourcegraph.cody.agent.CodyAgentService import com.sourcegraph.cody.auth.SelectOneOfTheAccountsAsActive import com.sourcegraph.cody.config.SettingsMigration @@ -29,7 +28,6 @@ class PostStartupActivity : StartupActivity.DumbAware { if (ConfigUtil.isCodyEnabled()) CodyAgentService.getInstance(project).startAgent(project) CodyAutocompleteStatusService.resetApplication(project) CodyFocusChangeListener().runActivity(project) - CodyAgentCodebase.getInstance(project).onFileOpened(project, null) EndOfTrialNotificationScheduler.createAndStart(project) } } diff --git a/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt b/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt index 7b03ceccfc..4465684e3e 100644 --- a/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt +++ b/src/main/kotlin/com/sourcegraph/config/ConfigUtil.kt @@ -45,7 +45,7 @@ object ConfigUtil { UserLevelConfig.getAutocompleteProviderType()?.vscodeSettingString(), debug = isCodyDebugEnabled(), verboseDebug = isCodyVerboseDebugEnabled(), - codebase = CodyAgentCodebase.getInstance(project).getUrl(), + codebase = CodyAgentCodebase.getInstance(project).getUrl().getNow(null), ) } diff --git a/src/main/resources/CodyBundle.properties b/src/main/resources/CodyBundle.properties index fae1e66382..033477e6f9 100644 --- a/src/main/resources/CodyBundle.properties +++ b/src/main/resources/CodyBundle.properties @@ -51,7 +51,6 @@ chat.attribution.success.label=Guard Rails Check Passed chat.attribution.success.tooltip=Snippet not found on Sourcegraph.com chat.attribution.failure.label=Guard Rails Check Failed chat.attribution.failure.tooltip=Guard Rails Check Failed. Code found in {0} repositories: {1} - my-account-tab.chat-rate-limit-error=You've used all your chat messages and commands for the month. Upgrade to Pro for unlimited usage. my-account-tab.autocomplete-rate-limit-error=You've used all your autocompletions for the month. Upgrade to Pro for unlimited usage. my-account-tab.chat-and-autocomplete-rate-limit-error=You've used all your autocompletions and chats for this month. Upgrade to Cody Pro to get unlimited interactions. @@ -59,7 +58,6 @@ my-account-tab.cody-pro-label=Cody Pro my-account-tab.cody-free-label=Cody Free my-account-tab.loading-label=Loading... my-account-tab.already-pro=(Already upgraded to Pro? Restart your IDE for changes to take effect) - commands-tab.message-in-progress=Message generation in progress... UpgradeToCodyProNotification.title.upgrade=You've used up your autocompletes for the month UpgradeToCodyProNotification.title.explain=Thank you for using Cody so heavily today! @@ -70,31 +68,33 @@ UpgradeToCodyProNotification.content.upgrade=\ (Already upgraded to Pro? Restart your IDE for changes to take effect)\ UpgradeToCodyProNotification.content.explain=To ensure that Cody can stay operational for all Cody users, please come back tomorrow for more chats, commands, and autocompletes. -context-panel.panel-name=Chat Context -context-panel.reindex-button-name=Trigger Reindexing +tab.subscription.already-pro=(Already upgraded to Pro? Restart your IDE for changes to take effect) +context-panel.button.remove-remote-repo=Remove Remote Repository +context-panel.button.add-remote-repo=Add Remote Repository +context-panel.button.reindex=Reindex Local Project +context-panel.button.save-default=Save Current Context As Default +context-panel.button.restore-default=Restore Default Context +context-panel.button.help=Help context-panel.in-progress=Running Cody 'Keyword Search' indexer... context-panel.error-title=Cody Failure context-panel.error-message=Cody 'Keyword Search' indexer failed: {0} -context-panel.help-button-name=Help -context-panel.tree-root=repositories-root -context-panel.tree-chat-context=Chat Context +context-panel.tree.root=repositories-root +context-panel.tree.node-chat-context=Chat Context +context-panel.tree.node-local-project=Local Project +context-panel.tree.node-remote-repos=Remote Repo(s) messages-panel.welcome-text=Hello! I'm Cody. I can write code and answer questions for you. See [Cody documentation](https://sourcegraph.com/docs/cody) for help and tips. chat-session.error-title=Error while processing the message chat-session.error-message=Cody is not able to reply at the moment. This is a bug, please report an issue to https://github.com/sourcegraph/jetbrains/issues/new/choose and include as many details as possible to help troubleshoot the problem. - -EndOfTrialNotification.link-action-name= Setup Payment Info +EndOfTrialNotification.link-action-name=Setup Payment Info EndOfTrialNotification.do-not-show-again=Don't show again - TrialEndingSoonNotification.ignore=cody.ignore.notification.trial-ending-soon TrialEndingSoonNotification.ending-soon.title=Your Cody Pro trial is ending soon TrialEndingSoonNotification.ending-soon.content=Setup your payment information to continue using Cody Pro, you won't be charged until February 15. TrialEndingSoonNotification.ending-soon.link=https://sourcegraph.com/cody/subscription?on-trial=true - TrialEndedNotification.ignore=cody.ignore.notification.trial-ended TrialEndedNotification.ended.title=Your Cody Pro trial has ended TrialEndedNotification.ended.content=Thank you for trying Cody Pro! Your trial period has ended. To keep enjoying all the features of Cody Pro, subscribe to our monthly plan. You can cancel anytime. TrialEndedNotification.ended.link=https://sourcegraph.com/cody/subscription - duration.today=Today duration.yesterday=Yesterday duration.x-days-ago={0} days ago @@ -105,7 +105,7 @@ duration.x-months-ago={0} months ago duration.last-year=Last year duration.x-years-ago={0} years ago duration.x-ago={0} ago - popup.select-chat=Select Chat popup.remove-chat=Remove Chat popup.remove-all-chats=Remove All Chats +