diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java index 15a0e533..782b539b 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -10,7 +10,9 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.SelectionModel; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.AnimatedIcon; import com.intellij.ui.JBColor; +import com.intellij.ui.components.JBLabel; import com.intellij.util.concurrency.AppExecutorUtil; import com.intellij.util.ui.JBUI; import ee.carlrobert.codegpt.CodeGPTKeys; @@ -39,7 +41,6 @@ import ee.carlrobert.codegpt.toolwindow.chat.editor.actions.CopyAction; import ee.carlrobert.codegpt.toolwindow.chat.structure.data.PsiStructureRepository; import ee.carlrobert.codegpt.toolwindow.chat.structure.data.PsiStructureState; import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatMessageResponseBody; -import ee.carlrobert.codegpt.toolwindow.chat.editor.header.LoadingPanel; import ee.carlrobert.codegpt.toolwindow.chat.ui.ChatToolWindowScrollablePanel; import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensDetails; import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel; @@ -62,6 +63,8 @@ import ee.carlrobert.codegpt.util.coroutines.CoroutineDispatchers; import ee.carlrobert.llm.client.openai.completion.ErrorDetails; import git4idea.GitCommit; import java.awt.BorderLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -95,7 +98,7 @@ public class ChatToolWindowTabPanel implements Disposable { private final TagManager tagManager; private final JPanel mcpApprovalContainer; private @Nullable ToolwindowChatCompletionRequestHandler requestHandler; - private LoadingPanel inputLoadingPanel; + private JBLabel loadingLabel; private final JPanel queuedMessageContainer; public ChatToolWindowTabPanel(@NotNull Project project, @NotNull Conversation conversation) { @@ -139,6 +142,9 @@ public class ChatToolWindowTabPanel implements Disposable { queuedMessageContainer.setBorder(JBUI.Borders.empty()); queuedMessageContainer.setOpaque(false); + loadingLabel = new JBLabel("", new AnimatedIcon.Default(), JBLabel.LEFT); + loadingLabel.setVisible(false); + rootPanel = createRootPanel(); if (conversation.getMessages().isEmpty()) { @@ -224,8 +230,9 @@ public class ChatToolWindowTabPanel implements Disposable { private void updateUserPromptPanel() { var userPromptPanel = createUserPromptPanel(); + rootPanel.remove(rootPanel.getComponent(rootPanel.getComponentCount() - 1)); - rootPanel.add(userPromptPanel, BorderLayout.SOUTH); + rootPanel.add(createSouthPanel(userPromptPanel), BorderLayout.SOUTH); rootPanel.revalidate(); rootPanel.repaint(); } @@ -603,11 +610,6 @@ public class ChatToolWindowTabPanel implements Disposable { topContainer.setLayout(new BoxLayout(topContainer, BoxLayout.Y_AXIS)); topContainer.setOpaque(false); - inputLoadingPanel = new LoadingPanel(CodeGPTBundle.get("toolwindow.chat.loading"), null, null); - inputLoadingPanel.setAlignmentX(JComponent.LEFT_ALIGNMENT); - inputLoadingPanel.setVisible(false); - topContainer.add(inputLoadingPanel); - if (queuedMessageContainer.getComponentCount() > 0) { queuedMessageContainer.setAlignmentX(JComponent.LEFT_ALIGNMENT); topContainer.add(queuedMessageContainer); @@ -618,30 +620,59 @@ public class ChatToolWindowTabPanel implements Disposable { topContainer.add(mcpApprovalContainer); } - var tokenPanelWrapper = JBUI.Panels.simplePanel(totalTokensPanel) - .withBorder(JBUI.Borders.empty(4, 0, 4, 0)); - tokenPanelWrapper.setAlignmentX(JComponent.LEFT_ALIGNMENT); - topContainer.add(tokenPanelWrapper); - panel.add(topContainer, BorderLayout.NORTH); panel.add(userInputPanel, BorderLayout.CENTER); return panel; } + private JComponent createStatusPanel() { + var statusPanel = new JPanel(new GridBagLayout()); + statusPanel.setBorder(JBUI.Borders.empty(8)); + statusPanel.setOpaque(false); + + var gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.anchor = GridBagConstraints.WEST; + gbc.weightx = 0; + gbc.fill = GridBagConstraints.NONE; + statusPanel.add(loadingLabel, gbc); + + gbc.gridx = 1; + gbc.weightx = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + statusPanel.add(Box.createHorizontalGlue(), gbc); + + gbc.gridx = 2; + gbc.weightx = 0; + gbc.anchor = GridBagConstraints.EAST; + gbc.fill = GridBagConstraints.NONE; + statusPanel.add(totalTokensPanel, gbc); + + return statusPanel; + } + + private JComponent createSouthPanel(JComponent userPromptPanel) { + var southPanel = new JPanel(new BorderLayout()); + southPanel.add(createStatusPanel(), BorderLayout.NORTH); + southPanel.add(userPromptPanel, BorderLayout.CENTER); + return southPanel; + } + private void showInputLoading(String text) { - if (inputLoadingPanel != null) { - inputLoadingPanel.setText(text); - inputLoadingPanel.setVisible(true); - inputLoadingPanel.revalidate(); - inputLoadingPanel.repaint(); + if (loadingLabel != null) { + loadingLabel.setText(text); + loadingLabel.setVisible(true); + rootPanel.revalidate(); + rootPanel.repaint(); } } private void hideInputLoading() { - if (inputLoadingPanel != null) { - inputLoadingPanel.setVisible(false); - inputLoadingPanel.revalidate(); - inputLoadingPanel.repaint(); + if (loadingLabel != null) { + loadingLabel.setVisible(false); + rootPanel.revalidate(); + rootPanel.repaint(); } } @@ -703,7 +734,7 @@ public class ChatToolWindowTabPanel implements Disposable { var rootPanel = new JPanel(new BorderLayout()); rootPanel.add(createScrollPaneWithSmartScroller(toolWindowScrollablePanel), BorderLayout.CENTER); - rootPanel.add(createUserPromptPanel(), BorderLayout.SOUTH); + rootPanel.add(createSouthPanel(createUserPromptPanel()), BorderLayout.SOUTH); return rootPanel; } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/ProxyAIChatCompletion.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/ProxyAIChatCompletion.kt index d5e8c348..1bcd99a3 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/ProxyAIChatCompletion.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/ProxyAIChatCompletion.kt @@ -47,7 +47,7 @@ public class ProxyAIChoice( public class ProxyAIStreamChoice( public val finishReason: String? = null, public val nativeFinishReason: String? = null, - public val delta: ProxyAIStreamDelta, + public val delta: ProxyAIStreamDelta?, public val error: ProxyAIErrorResponse? = null ) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/ProxyAILLMClient.kt b/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/ProxyAILLMClient.kt index 7e937a98..eab9199c 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/ProxyAILLMClient.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/agent/clients/ProxyAILLMClient.kt @@ -130,8 +130,8 @@ public class ProxyAILLMClient( override suspend fun StreamFrameFlowBuilder.processStreamingChunk(chunk: ProxyAIChatCompletionStreamResponse) { chunk.choices.firstOrNull()?.let { choice -> - choice.delta.content?.let { emitAppend(it) } - choice.delta.toolCalls?.forEachIndexed { index, toolCall -> + choice.delta?.content?.let { emitAppend(it) } + choice.delta?.toolCalls?.forEachIndexed { index, toolCall -> val id = toolCall.id val name = toolCall.function.name val arguments = toolCall.function.arguments diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/diff/DiffSyncManager.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/diff/DiffSyncManager.kt index fec971d4..35c7c6de 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/diff/DiffSyncManager.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/editor/diff/DiffSyncManager.kt @@ -1,9 +1,12 @@ package ee.carlrobert.codegpt.toolwindow.chat.editor.diff import com.intellij.diff.util.Side +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.ReadAction import com.intellij.openapi.application.runInEdt import com.intellij.openapi.application.runReadAction import com.intellij.openapi.application.runUndoTransparentWriteAction +import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.event.DocumentEvent import com.intellij.openapi.editor.event.DocumentListener import com.intellij.openapi.editor.ex.EditorEx @@ -11,6 +14,7 @@ import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.util.application +import com.intellij.util.concurrency.AppExecutorUtil import ee.carlrobert.codegpt.toolwindow.chat.editor.ResponseEditorPanel.Companion.RESPONSE_EDITOR_DIFF_VIEWER_KEY import ee.carlrobert.codegpt.toolwindow.chat.editor.ResponseEditorPanel.Companion.RESPONSE_EDITOR_DIFF_VIEWER_VALUE_PAIR_KEY import java.util.concurrent.ConcurrentHashMap @@ -25,48 +29,41 @@ object DiffSyncManager { (set ?: mutableSetOf()).apply { add(editor) } } - if (!fileToListener.containsKey(filePath)) { - val virtualFile = LocalFileSystem.getInstance().findFileByPath(filePath) ?: return - val document = - runReadAction { FileDocumentManager.getInstance().getDocument(virtualFile) } - ?: return + val listener = object : DocumentListener { + override fun documentChanged(event: DocumentEvent) { + application.executeOnPooledThread { + val affectedEditors = fileToEditors[filePath] ?: emptyList() + for (editor in affectedEditors) { + val diffViewer = RESPONSE_EDITOR_DIFF_VIEWER_KEY.get(editor) + if (diffViewer != null) { + val leftSideDoc = + runReadAction { diffViewer.getDocument(Side.LEFT) } + val rightSideDoc = + runReadAction { diffViewer.getDocument(Side.RIGHT) } - val listener = object : DocumentListener { - override fun documentChanged(event: DocumentEvent) { - application.executeOnPooledThread { - val affectedEditors = fileToEditors[filePath] ?: emptyList() - for (editor in affectedEditors) { - val diffViewer = RESPONSE_EDITOR_DIFF_VIEWER_KEY.get(editor) - if (diffViewer != null) { - val leftSideDoc = - runReadAction { diffViewer.getDocument(Side.LEFT) } - val rightSideDoc = - runReadAction { diffViewer.getDocument(Side.RIGHT) } + if (leftSideDoc.text == rightSideDoc.text) { + continue + } - if (leftSideDoc.text == rightSideDoc.text) { - continue - } - - val entry = RESPONSE_EDITOR_DIFF_VIEWER_VALUE_PAIR_KEY.get(editor) - if (entry != null) { - val (search, replace) = entry - val newText = event.document.text - if (!newText.contains(replace.trim())) { - val replacedText = - newText.replace(search.trim(), replace.trim()) - runInEdt { - if (replacedText.length != newText.length) { - runUndoTransparentWriteAction { - rightSideDoc.setText( - StringUtil.convertLineSeparators( - replacedText - ) + val entry = RESPONSE_EDITOR_DIFF_VIEWER_VALUE_PAIR_KEY.get(editor) + if (entry != null) { + val (search, replace) = entry + val newText = event.document.text + if (!newText.contains(replace.trim())) { + val replacedText = + newText.replace(search.trim(), replace.trim()) + runInEdt { + if (replacedText.length != newText.length) { + runUndoTransparentWriteAction { + rightSideDoc.setText( + StringUtil.convertLineSeparators( + replacedText ) - diffViewer.scheduleRediff() - } + ) + diffViewer.scheduleRediff() } - diffViewer.rediff(true) } + diffViewer.rediff(true) } } } @@ -74,10 +71,32 @@ object DiffSyncManager { } } } + } + + val existing = fileToListener.putIfAbsent(filePath, listener) + if (existing != null) { + return + } + + ReadAction.nonBlocking { + val virtualFile = LocalFileSystem.getInstance().findFileByPath(filePath) + if (virtualFile == null) { + null + } else { + FileDocumentManager.getInstance().getDocument(virtualFile) + } + }.finishOnUiThread(ModalityState.any()) { document -> + if (document == null || fileToEditors[filePath].isNullOrEmpty()) { + fileToListener.remove(filePath, listener) + return@finishOnUiThread + } + + if (fileToListener[filePath] != listener) { + return@finishOnUiThread + } document.addDocumentListener(listener) - fileToListener[filePath] = listener - } + }.submit(AppExecutorUtil.getAppExecutorService()) } fun unregisterEditor(filePath: String, editor: EditorEx) { @@ -85,15 +104,23 @@ object DiffSyncManager { set.remove(editor) if (set.isEmpty()) { fileToEditors.remove(filePath) - - val virtualFile = LocalFileSystem.getInstance().findFileByPath(filePath) - val document = - virtualFile?.let { FileDocumentManager.getInstance().getDocument(it) } val listener = fileToListener.remove(filePath) - if (document != null && listener != null) { - document.removeDocumentListener(listener) + + if (listener != null) { + ReadAction.nonBlocking { + val virtualFile = LocalFileSystem.getInstance().findFileByPath(filePath) + if (virtualFile == null) { + null + } else { + FileDocumentManager.getInstance().getDocument(virtualFile) + } + }.finishOnUiThread(ModalityState.any()) { document -> + if (document != null) { + document.removeDocumentListener(listener) + } + }.submit(AppExecutorUtil.getAppExecutorService()) } } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.kt index 974fe0ee..c55265d6 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/TotalTokensPanel.kt @@ -64,7 +64,7 @@ class TotalTokensPanel( } } - border = JBUI.Borders.empty(4) + border = JBUI.Borders.empty() isOpaque = false add(JBLabel(AllIcons.General.ContextHelp).apply { addMouseListener(object : MouseAdapter() {