From 24e49e9e0b25c2b533226d634adbd1804c8c4a65 Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Sun, 30 Nov 2025 12:45:20 +0800 Subject: [PATCH 1/4] feat(mpp-idea): add advanced DevIn input components - Add IdeaInputListener interface for input events - Add IdeaDevInInput with EditorTextField and DevIn language support - Add IdeaBottomToolbar with Jewel-based send/stop/@/settings buttons - Add IdeaInputSection combining input with toolbar - Update IdeaAgentApp to use new IdeaInputSection Features: - Multi-line text input with Enter to send, Shift+Enter for newline - DevIn language syntax highlighting and completion - LookupManager integration for completion popup handling - Workspace path and token count display --- .../devins/idea/editor/IdeaBottomToolbar.kt | 162 +++++++++++++ .../devins/idea/editor/IdeaDevInInput.kt | 222 ++++++++++++++++++ .../devins/idea/editor/IdeaInputListener.kt | 39 +++ .../devins/idea/editor/IdeaInputSection.kt | 157 +++++++++++++ .../devins/idea/toolwindow/IdeaAgentApp.kt | 89 +++---- 5 files changed, 614 insertions(+), 55 deletions(-) create mode 100644 mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaBottomToolbar.kt create mode 100644 mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt create mode 100644 mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputListener.kt create mode 100644 mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputSection.kt diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaBottomToolbar.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaBottomToolbar.kt new file mode 100644 index 0000000000..d52e8ae907 --- /dev/null +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaBottomToolbar.kt @@ -0,0 +1,162 @@ +package cc.unitmesh.devins.idea.editor + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cc.unitmesh.devins.ui.compose.theme.AutoDevColors +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.component.* + +/** + * Bottom toolbar for the input section. + * Provides send/stop buttons, @ trigger for agent completion, settings, and token info. + * + * Uses Jewel components for native IntelliJ IDEA look and feel. + */ +@Composable +fun IdeaBottomToolbar( + onSendClick: () -> Unit, + sendEnabled: Boolean, + isExecuting: Boolean = false, + onStopClick: () -> Unit = {}, + onAtClick: () -> Unit = {}, + onSettingsClick: () -> Unit = {}, + workspacePath: String? = null, + totalTokens: Int? = null, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 6.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + // Left side: workspace and token info + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f, fill = false) + ) { + // Workspace indicator + if (!workspacePath.isNullOrEmpty()) { + val projectName = workspacePath.substringAfterLast('/') + .ifEmpty { workspacePath.substringAfterLast('\\') } + .ifEmpty { "Project" } + + Box( + modifier = Modifier + .background( + JewelTheme.globalColors.panelBackground.copy(alpha = 0.8f) + ) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = "📁", + style = JewelTheme.defaultTextStyle.copy(fontSize = 12.sp) + ) + Text( + text = projectName, + style = JewelTheme.defaultTextStyle.copy(fontSize = 12.sp), + maxLines = 1 + ) + } + } + } + + // Token usage indicator + if (totalTokens != null && totalTokens > 0) { + Box( + modifier = Modifier + .background(AutoDevColors.Blue.c400.copy(alpha = 0.2f)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = "Token", + style = JewelTheme.defaultTextStyle.copy(fontSize = 11.sp) + ) + Text( + text = "$totalTokens", + style = JewelTheme.defaultTextStyle.copy( + fontSize = 11.sp, + fontWeight = FontWeight.Bold + ) + ) + } + } + } + } + + // Right side: action buttons + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // @ trigger button for agent completion + IconButton( + onClick = onAtClick, + modifier = Modifier.size(32.dp) + ) { + Text( + text = "@", + style = JewelTheme.defaultTextStyle.copy( + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + ) + } + + // Settings button + IconButton( + onClick = onSettingsClick, + modifier = Modifier.size(32.dp) + ) { + Text( + text = "⚙", + style = JewelTheme.defaultTextStyle.copy(fontSize = 14.sp) + ) + } + + // Send or Stop button + if (isExecuting) { + DefaultButton( + onClick = onStopClick, + modifier = Modifier.height(32.dp) + ) { + Text( + text = "⏹ Stop", + style = JewelTheme.defaultTextStyle.copy( + color = AutoDevColors.Red.c400 + ) + ) + } + } else { + DefaultButton( + onClick = onSendClick, + enabled = sendEnabled, + modifier = Modifier.height(32.dp) + ) { + Text( + text = "Send", + style = JewelTheme.defaultTextStyle + ) + } + } + } + } +} + diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt new file mode 100644 index 0000000000..b106694fa9 --- /dev/null +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt @@ -0,0 +1,222 @@ +package cc.unitmesh.devins.idea.editor + +import com.intellij.codeInsight.lookup.LookupManagerListener +import com.intellij.lang.Language +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.CustomShortcutSet +import com.intellij.openapi.actionSystem.KeyboardShortcut +import com.intellij.openapi.command.CommandProcessor +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorModificationUtil +import com.intellij.openapi.editor.actions.IncrementalFindAction +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileTypes.FileTypes +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.testFramework.LightVirtualFile +import com.intellij.ui.EditorTextField +import com.intellij.util.EventDispatcher +import com.intellij.util.ui.JBUI +import java.awt.Color +import java.awt.event.KeyEvent +import java.util.* +import javax.swing.KeyStroke + +/** + * DevIn language input component for mpp-idea module. + * + * Features: + * - DevIn language support with syntax highlighting and completion + * - Enter to submit, Shift/Ctrl/Cmd+Enter for newline + * - Integration with IntelliJ's completion system (lookup listener) + * - Placeholder text support + * + * Based on AutoDevInput from core module but adapted for standalone mpp-idea usage. + */ +class IdeaDevInInput( + project: Project, + private val listeners: List = emptyList(), + val disposable: Disposable?, + private val showAgent: Boolean = true +) : EditorTextField(project, FileTypes.PLAIN_TEXT), Disposable { + + private val editorListeners = EventDispatcher.create(IdeaInputListener::class.java) + + // Enter key handling - submit on Enter, newline on Shift/Ctrl/Cmd+Enter + private val submitAction = DumbAwareAction.create { + val text = text.trim() + if (text.isNotEmpty()) { + editorListeners.multicaster.onSubmit(text, IdeaInputTrigger.Key) + } + } + + private val enterShortcutSet = CustomShortcutSet( + KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), null) + ) + + private val newlineAction = DumbAwareAction.create { + val editor = editor ?: return@create + insertNewLine(editor) + } + + private fun insertNewLine(editor: Editor) { + CommandProcessor.getInstance().executeCommand(project, { + val eol = "\n" + val document = editor.document + val caretOffset = editor.caretModel.offset + val lineEndOffset = document.getLineEndOffset(document.getLineNumber(caretOffset)) + val textAfterCaret = document.getText(TextRange(caretOffset, lineEndOffset)) + + WriteCommandAction.runWriteCommandAction(project) { + if (textAfterCaret.isBlank()) { + document.insertString(caretOffset, eol) + EditorModificationUtil.moveCaretRelatively(editor, 1) + } else { + document.insertString(caretOffset, eol) + editor.caretModel.moveToOffset(caretOffset + eol.length) + } + } + }, "Insert New Line", null) + } + + init { + isOneLineMode = false + setPlaceholder("Type your message or /help for commands...") + setFontInheritedFromLAF(true) + + addSettingsProvider { + it.putUserData(IncrementalFindAction.SEARCH_DISABLED, true) + it.colorsScheme.lineSpacing = 1.2f + it.settings.isUseSoftWraps = true + it.isEmbeddedIntoDialogWrapper = true + it.contentComponent.setOpaque(false) + } + + background = EditorColorsManager.getInstance().globalScheme.defaultBackground + + registerEnterShortcut() + + // Register newline shortcuts: Ctrl+Enter, Cmd+Enter, Shift+Enter + newlineAction.registerCustomShortcutSet( + CustomShortcutSet( + KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_DOWN_MASK), null), + KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.META_DOWN_MASK), null), + KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.SHIFT_DOWN_MASK), null), + ), this + ) + + listeners.forEach { listener -> + document.addDocumentListener(listener) + } + + // Listen for completion popup state to disable Enter submit when completing + project.messageBus.connect(disposable ?: this) + .subscribe(LookupManagerListener.TOPIC, object : LookupManagerListener { + override fun activeLookupChanged( + oldLookup: com.intellij.codeInsight.lookup.Lookup?, + newLookup: com.intellij.codeInsight.lookup.Lookup? + ) { + if (newLookup != null) { + unregisterEnterShortcut() + } else { + registerEnterShortcut() + } + } + }) + } + + private fun registerEnterShortcut() { + submitAction.registerCustomShortcutSet(enterShortcutSet, this) + } + + private fun unregisterEnterShortcut() { + submitAction.unregisterCustomShortcutSet(this) + } + + override fun onEditorAdded(editor: Editor) { + editorListeners.multicaster.editorAdded(editor as EditorEx) + } + + public override fun createEditor(): EditorEx { + val editor = super.createEditor() + editor.setVerticalScrollbarVisible(true) + setBorder(JBUI.Borders.empty()) + editor.setShowPlaceholderWhenFocused(true) + editor.caretModel.moveToOffset(0) + editor.scrollPane.setBorder(border) + editor.contentComponent.setOpaque(false) + return editor + } + + override fun getBackground(): Color { + val editor = editor ?: return super.getBackground() + return editor.colorsScheme.defaultBackground + } + + override fun dispose() { + listeners.forEach { + editor?.document?.removeDocumentListener(it) + } + } + + /** + * Recreate the document with DevIn language support. + * This enables syntax highlighting and completion for DevIn commands. + */ + fun recreateDocument() { + val language = Language.findLanguageByID("DevIn") ?: Language.ANY + val id = UUID.randomUUID() + val file = LightVirtualFile("IdeaDevInInput-$id", language, "") + + val document = FileDocumentManager.getInstance().getDocument(file) + ?: throw IllegalStateException("Can't create in-memory document") + + initializeDocumentListeners(document) + setDocument(document) + } + + private fun initializeDocumentListeners(inputDocument: Document) { + listeners.forEach { listener -> + inputDocument.addDocumentListener(listener) + } + } + + /** + * Add a listener for input events. + */ + fun addInputListener(listener: IdeaInputListener) { + editorListeners.addListener(listener) + } + + /** + * Remove a listener. + */ + fun removeInputListener(listener: IdeaInputListener) { + editorListeners.removeListener(listener) + } + + /** + * Append text at the end of the document. + */ + fun appendText(textToAppend: String) { + WriteCommandAction.runWriteCommandAction(project, "Append text", "intentions.write.action", { + val document = this.editor?.document ?: return@runWriteCommandAction + document.insertString(document.textLength, textToAppend) + }) + } + + /** + * Clear the input and recreate document. + */ + fun clearInput() { + recreateDocument() + text = "" + } +} + diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputListener.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputListener.kt new file mode 100644 index 0000000000..fbf4cbf945 --- /dev/null +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputListener.kt @@ -0,0 +1,39 @@ +package cc.unitmesh.devins.idea.editor + +import com.intellij.openapi.editor.ex.EditorEx +import java.util.* + +/** + * Trigger type for input submission. + */ +enum class IdeaInputTrigger { + Button, + Key +} + +/** + * Listener interface for input events from IdeaDevInInput. + * Modeled after AutoDevInputListener from core module. + */ +interface IdeaInputListener : EventListener { + /** + * Called when the editor is added to the component. + */ + fun editorAdded(editor: EditorEx) {} + + /** + * Called when user submits input (via Enter key or Send button). + */ + fun onSubmit(text: String, trigger: IdeaInputTrigger) {} + + /** + * Called when user requests to stop current execution. + */ + fun onStop() {} + + /** + * Called when text content changes. + */ + fun onTextChanged(text: String) {} +} + diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputSection.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputSection.kt new file mode 100644 index 0000000000..846adfba7c --- /dev/null +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputSection.kt @@ -0,0 +1,157 @@ +package cc.unitmesh.devins.idea.editor + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.* +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.flow.distinctUntilChanged +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.Orientation +import org.jetbrains.jewel.ui.component.* + +/** + * Complete input section for mpp-idea module. + * + * Combines a text input field with a bottom toolbar for actions. + * Uses Jewel components for native IntelliJ IDEA integration. + * + * Features: + * - Multi-line text input with DevIn command support + * - Enter to submit, Shift+Enter for newline + * - Bottom toolbar with send/stop, @ trigger, settings + * - Workspace and token info display + * + * Note: This is a pure Compose implementation. For full DevIn language support + * with completion, use IdeaDevInInput (Swing-based) embedded via ComposePanel. + */ +@Composable +fun IdeaInputSection( + isProcessing: Boolean, + onSend: (String) -> Unit, + onStop: () -> Unit = {}, + onAtClick: () -> Unit = {}, + onSettingsClick: () -> Unit = {}, + workspacePath: String? = null, + totalTokens: Int? = null, + modifier: Modifier = Modifier +) { + val textFieldState = rememberTextFieldState() + var inputText by remember { mutableStateOf("") } + + // Sync text field state to inputText + LaunchedEffect(Unit) { + snapshotFlow { textFieldState.text.toString() } + .distinctUntilChanged() + .collect { inputText = it } + } + + Column( + modifier = modifier + .fillMaxWidth() + .background(JewelTheme.globalColors.panelBackground) + .border( + width = 1.dp, + color = JewelTheme.globalColors.borders.normal, + shape = RoundedCornerShape(4.dp) + ) + ) { + // Input area + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 60.dp, max = 200.dp) + .padding(8.dp) + ) { + TextField( + state = textFieldState, + placeholder = { + Text( + text = "Type your message or /help for commands...", + style = JewelTheme.defaultTextStyle.copy( + fontSize = 14.sp, + color = JewelTheme.globalColors.text.info.copy(alpha = 0.6f) + ) + ) + }, + modifier = Modifier + .fillMaxWidth() + .onPreviewKeyEvent { keyEvent -> + // Enter to send (without modifiers) + if (keyEvent.key == Key.Enter && + keyEvent.type == KeyEventType.KeyDown && + !keyEvent.isShiftPressed && + !keyEvent.isCtrlPressed && + !keyEvent.isMetaPressed && + !isProcessing + ) { + if (inputText.isNotBlank()) { + onSend(inputText) + textFieldState.edit { replace(0, length, "") } + } + true + } else { + false + } + }, + enabled = !isProcessing + ) + } + + Divider(Orientation.Horizontal, modifier = Modifier.fillMaxWidth()) + + // Bottom toolbar + IdeaBottomToolbar( + onSendClick = { + if (inputText.isNotBlank()) { + onSend(inputText) + textFieldState.edit { replace(0, length, "") } + } + }, + sendEnabled = inputText.isNotBlank() && !isProcessing, + isExecuting = isProcessing, + onStopClick = onStop, + onAtClick = { + // Insert @ character and trigger completion + textFieldState.edit { + append("@") + } + onAtClick() + }, + onSettingsClick = onSettingsClick, + workspacePath = workspacePath, + totalTokens = totalTokens + ) + } +} + +/** + * Preview hints display for available commands. + */ +@Composable +fun InputHints( + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.End + ) { + Text( + text = "Enter to send, Shift+Enter for newline", + style = JewelTheme.defaultTextStyle.copy( + fontSize = 11.sp, + color = JewelTheme.globalColors.text.info.copy(alpha = 0.6f) + ) + ) + } +} + diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt index 769931bc9d..1669e9bcd1 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt @@ -131,7 +131,14 @@ fun IdeaAgentApp( ChatInputArea( isProcessing = isExecuting, onSend = { viewModel.sendMessage(it) }, - onAbort = { viewModel.cancelTask() } + onAbort = { viewModel.cancelTask() }, + workspacePath = project.basePath, + totalTokens = null, // TODO: integrate token counting from renderer + onSettingsClick = { viewModel.setShowConfigDialog(true) }, + onAtClick = { + // @ click triggers agent completion - for now just a placeholder + // Full completion integration requires EditorTextField with DevIn language + } ) } @@ -608,64 +615,36 @@ private fun ToolStatusChip( } } +/** + * Advanced chat input area with full DevIn language support. + * + * Features: + * - Multi-line text input with syntax highlighting hints + * - Enter to submit, Shift+Enter for newline + * - @ trigger for agent completion + * - Token usage display + * - Settings access + * - Stop/Send button based on execution state + */ @Composable private fun ChatInputArea( isProcessing: Boolean, onSend: (String) -> Unit, - onAbort: () -> Unit + onAbort: () -> Unit, + workspacePath: String? = null, + totalTokens: Int? = null, + onSettingsClick: () -> Unit = {}, + onAtClick: () -> Unit = {} ) { - val textFieldState = rememberTextFieldState() - var inputText by remember { mutableStateOf("") } - - LaunchedEffect(Unit) { - snapshotFlow { textFieldState.text.toString() } - .distinctUntilChanged() - .collect { inputText = it } - } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - TextField( - state = textFieldState, - placeholder = { Text("Type your message or /help for commands...") }, - modifier = Modifier - .weight(1f) - .onPreviewKeyEvent { keyEvent -> - if (keyEvent.key == Key.Enter && keyEvent.type == KeyEventType.KeyDown && !isProcessing) { - if (inputText.isNotBlank()) { - onSend(inputText) - textFieldState.edit { replace(0, length, "") } - } - true - } else { - false - } - }, - enabled = !isProcessing - ) - - if (isProcessing) { - DefaultButton(onClick = onAbort) { - Text("Stop") - } - } else { - DefaultButton( - onClick = { - if (inputText.isNotBlank()) { - onSend(inputText) - textFieldState.edit { replace(0, length, "") } - } - }, - enabled = inputText.isNotBlank() - ) { - Text("Send") - } - } - } + cc.unitmesh.devins.idea.editor.IdeaInputSection( + isProcessing = isProcessing, + onSend = onSend, + onStop = onAbort, + onAtClick = onAtClick, + onSettingsClick = onSettingsClick, + workspacePath = workspacePath, + totalTokens = totalTokens, + modifier = Modifier.padding(8.dp) + ) } From 9fe3fafee72d5c48d81829ff8819746052bb0809 Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Sun, 30 Nov 2025 12:51:58 +0800 Subject: [PATCH 2/4] feat(mpp-idea): integrate IdeaDevInInput via SwingPanel - Replace pure Compose IdeaInputSection with SwingPanel-embedded IdeaDevInInput - DevInInputArea now embeds EditorTextField for full DevIn language support - Add text change listener to IdeaDevInInput for Compose state sync - Combine SwingPanel editor with Compose bottom toolbar This enables IntelliJ native completion popup and DevIn syntax highlighting. --- .../devins/idea/editor/IdeaDevInInput.kt | 7 ++ .../devins/idea/toolwindow/IdeaAgentApp.kt | 117 +++++++++++++++--- 2 files changed, 108 insertions(+), 16 deletions(-) diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt index b106694fa9..e7d9a44649 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt @@ -115,6 +115,13 @@ class IdeaDevInInput( document.addDocumentListener(listener) } + // Add internal document listener to notify text changes + document.addDocumentListener(object : DocumentListener { + override fun documentChanged(event: com.intellij.openapi.editor.event.DocumentEvent) { + editorListeners.multicaster.onTextChanged(text) + } + }) + // Listen for completion popup state to disable Enter submit when completing project.messageBus.connect(disposable ?: this) .subscribe(LookupManagerListener.TOPIC, object : LookupManagerListener { diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt index 1669e9bcd1..667af75c38 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt @@ -10,23 +10,34 @@ import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.SwingPanel import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import cc.unitmesh.agent.AgentType +import cc.unitmesh.devins.idea.editor.IdeaBottomToolbar +import cc.unitmesh.devins.idea.editor.IdeaDevInInput +import cc.unitmesh.devins.idea.editor.IdeaInputListener +import cc.unitmesh.devins.idea.editor.IdeaInputTrigger import cc.unitmesh.devins.idea.renderer.JewelRenderer import cc.unitmesh.devins.idea.toolwindow.codereview.IdeaCodeReviewContent import cc.unitmesh.devins.idea.toolwindow.codereview.IdeaCodeReviewViewModel import cc.unitmesh.devins.ui.compose.theme.AutoDevColors +import com.intellij.openapi.Disposable +import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.Orientation import org.jetbrains.jewel.ui.component.* import org.jetbrains.jewel.ui.theme.defaultBannerStyle +import java.awt.BorderLayout +import java.awt.Dimension +import javax.swing.JPanel /** * Main Compose application for Agent ToolWindow. @@ -128,7 +139,9 @@ fun IdeaAgentApp( // Input area (only for chat-based modes) if (currentAgentType == AgentType.CODING || currentAgentType == AgentType.REMOTE || currentAgentType == AgentType.LOCAL_CHAT) { - ChatInputArea( + DevInInputArea( + project = project, + parentDisposable = viewModel, isProcessing = isExecuting, onSend = { viewModel.sendMessage(it) }, onAbort = { viewModel.cancelTask() }, @@ -136,8 +149,7 @@ fun IdeaAgentApp( totalTokens = null, // TODO: integrate token counting from renderer onSettingsClick = { viewModel.setShowConfigDialog(true) }, onAtClick = { - // @ click triggers agent completion - for now just a placeholder - // Full completion integration requires EditorTextField with DevIn language + // @ click triggers agent completion - placeholder for now } ) } @@ -618,8 +630,9 @@ private fun ToolStatusChip( /** * Advanced chat input area with full DevIn language support. * - * Features: - * - Multi-line text input with syntax highlighting hints + * Uses IdeaDevInInput (EditorTextField-based) embedded via SwingPanel for: + * - DevIn language syntax highlighting and completion + * - IntelliJ's native completion popup integration * - Enter to submit, Shift+Enter for newline * - @ trigger for agent completion * - Token usage display @@ -627,7 +640,9 @@ private fun ToolStatusChip( * - Stop/Send button based on execution state */ @Composable -private fun ChatInputArea( +private fun DevInInputArea( + project: Project, + parentDisposable: Disposable, isProcessing: Boolean, onSend: (String) -> Unit, onAbort: () -> Unit, @@ -636,15 +651,85 @@ private fun ChatInputArea( onSettingsClick: () -> Unit = {}, onAtClick: () -> Unit = {} ) { - cc.unitmesh.devins.idea.editor.IdeaInputSection( - isProcessing = isProcessing, - onSend = onSend, - onStop = onAbort, - onAtClick = onAtClick, - onSettingsClick = onSettingsClick, - workspacePath = workspacePath, - totalTokens = totalTokens, - modifier = Modifier.padding(8.dp) - ) + var inputText by remember { mutableStateOf("") } + var devInInput by remember { mutableStateOf(null) } + + Column( + modifier = Modifier.fillMaxWidth().padding(8.dp) + ) { + // DevIn Editor via SwingPanel + SwingPanel( + modifier = Modifier + .fillMaxWidth() + .height(120.dp), + factory = { + val input = IdeaDevInInput( + project = project, + disposable = parentDisposable, + showAgent = true + ).apply { + recreateDocument() + + addInputListener(object : IdeaInputListener { + override fun editorAdded(editor: EditorEx) { + // Editor is ready + } + + override fun onSubmit(text: String, trigger: IdeaInputTrigger) { + if (text.isNotBlank() && !isProcessing) { + onSend(text) + clearInput() + inputText = "" + } + } + + override fun onStop() { + onAbort() + } + + override fun onTextChanged(text: String) { + inputText = text + } + }) + } + + // Register for disposal + Disposer.register(parentDisposable, input) + devInInput = input + + // Wrap in a JPanel to handle sizing + JPanel(BorderLayout()).apply { + add(input, BorderLayout.CENTER) + preferredSize = Dimension(800, 120) + minimumSize = Dimension(200, 80) + } + }, + update = { panel -> + // Update panel if needed + } + ) + + // Bottom toolbar with Compose + IdeaBottomToolbar( + onSendClick = { + val text = devInInput?.text?.trim() ?: inputText.trim() + if (text.isNotBlank() && !isProcessing) { + onSend(text) + devInInput?.clearInput() + inputText = "" + } + }, + sendEnabled = inputText.isNotBlank() && !isProcessing, + isExecuting = isProcessing, + onStopClick = onAbort, + onAtClick = { + devInInput?.appendText("@") + onAtClick() + }, + onSettingsClick = onSettingsClick, + workspacePath = workspacePath, + totalTokens = totalTokens + ) + } } From d41e8ffbc75e5ff7149b820dcf69d5c85f67db98 Mon Sep 17 00:00:00 2001 From: Fengda Huang Date: Sun, 30 Nov 2025 06:46:52 +0000 Subject: [PATCH 3/4] fix: address code review comments - Fix memory leak by storing and properly disposing internal DocumentListener - Fix recreateDocument to use EditorFactory instead of FileDocumentManager - Re-attach listeners when document is recreated - Fix Windows path handling in project name extraction - Remove unused imports (Color, java.util.*) - Extract duplicate send logic to avoid code duplication - Remove redundant text assignment in clearInput() Addresses review comments from PR #7 --- .../devins/idea/editor/IdeaBottomToolbar.kt | 7 ++-- .../devins/idea/editor/IdeaDevInInput.kt | 42 ++++++++++++------- .../devins/idea/editor/IdeaInputListener.kt | 2 +- .../devins/idea/editor/IdeaInputSection.kt | 23 +++++----- 4 files changed, 43 insertions(+), 31 deletions(-) diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaBottomToolbar.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaBottomToolbar.kt index d52e8ae907..3469e91972 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaBottomToolbar.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaBottomToolbar.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -46,8 +45,10 @@ fun IdeaBottomToolbar( ) { // Workspace indicator if (!workspacePath.isNullOrEmpty()) { - val projectName = workspacePath.substringAfterLast('/') - .ifEmpty { workspacePath.substringAfterLast('\\') } + // Extract project name from path, handling both Unix and Windows separators + val projectName = workspacePath + .replace('\\', '/') // Normalize to Unix separator + .substringAfterLast('/') .ifEmpty { "Project" } Box( diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt index e7d9a44649..3cc8f36784 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt @@ -9,12 +9,13 @@ import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.editor.EditorModificationUtil +import com.intellij.openapi.application.ReadAction import com.intellij.openapi.editor.actions.IncrementalFindAction import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.editor.event.DocumentListener import com.intellij.openapi.editor.ex.EditorEx -import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileTypes.FileTypes import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project @@ -45,9 +46,16 @@ class IdeaDevInInput( val disposable: Disposable?, private val showAgent: Boolean = true ) : EditorTextField(project, FileTypes.PLAIN_TEXT), Disposable { - + private val editorListeners = EventDispatcher.create(IdeaInputListener::class.java) - + + // Internal document listener to notify text changes + private val internalDocumentListener = object : DocumentListener { + override fun documentChanged(event: com.intellij.openapi.editor.event.DocumentEvent) { + editorListeners.multicaster.onTextChanged(text) + } + } + // Enter key handling - submit on Enter, newline on Shift/Ctrl/Cmd+Enter private val submitAction = DumbAwareAction.create { val text = text.trim() @@ -55,7 +63,7 @@ class IdeaDevInInput( editorListeners.multicaster.onSubmit(text, IdeaInputTrigger.Key) } } - + private val enterShortcutSet = CustomShortcutSet( KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), null) ) @@ -116,11 +124,7 @@ class IdeaDevInInput( } // Add internal document listener to notify text changes - document.addDocumentListener(object : DocumentListener { - override fun documentChanged(event: com.intellij.openapi.editor.event.DocumentEvent) { - editorListeners.multicaster.onTextChanged(text) - } - }) + document.addDocumentListener(internalDocumentListener) // Listen for completion popup state to disable Enter submit when completing project.messageBus.connect(disposable ?: this) @@ -167,6 +171,7 @@ class IdeaDevInInput( } override fun dispose() { + editor?.document?.removeDocumentListener(internalDocumentListener) listeners.forEach { editor?.document?.removeDocumentListener(it) } @@ -177,12 +182,18 @@ class IdeaDevInInput( * This enables syntax highlighting and completion for DevIn commands. */ fun recreateDocument() { - val language = Language.findLanguageByID("DevIn") ?: Language.ANY - val id = UUID.randomUUID() - val file = LightVirtualFile("IdeaDevInInput-$id", language, "") + // Remove listeners from old document before replacing + editor?.document?.let { oldDoc -> + oldDoc.removeDocumentListener(internalDocumentListener) + listeners.forEach { listener -> + oldDoc.removeDocumentListener(listener) + } + } - val document = FileDocumentManager.getInstance().getDocument(file) - ?: throw IllegalStateException("Can't create in-memory document") + // Create new document using EditorFactory + val document = ReadAction.compute { + EditorFactory.getInstance().createDocument("") + } initializeDocumentListeners(document) setDocument(document) @@ -192,6 +203,8 @@ class IdeaDevInInput( listeners.forEach { listener -> inputDocument.addDocumentListener(listener) } + // Re-add internal listener to new document + inputDocument.addDocumentListener(internalDocumentListener) } /** @@ -223,7 +236,6 @@ class IdeaDevInInput( */ fun clearInput() { recreateDocument() - text = "" } } diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputListener.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputListener.kt index fbf4cbf945..b72d78bac0 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputListener.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputListener.kt @@ -1,7 +1,7 @@ package cc.unitmesh.devins.idea.editor import com.intellij.openapi.editor.ex.EditorEx -import java.util.* +import java.util.EventListener /** * Trigger type for input submission. diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputSection.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputSection.kt index 846adfba7c..8b20f968a2 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputSection.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputSection.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -53,6 +52,14 @@ fun IdeaInputSection( .collect { inputText = it } } + // Extract send logic to avoid duplication + val doSend: () -> Unit = { + if (inputText.isNotBlank()) { + onSend(inputText) + textFieldState.edit { replace(0, length, "") } + } + } + Column( modifier = modifier .fillMaxWidth() @@ -85,17 +92,14 @@ fun IdeaInputSection( .fillMaxWidth() .onPreviewKeyEvent { keyEvent -> // Enter to send (without modifiers) - if (keyEvent.key == Key.Enter && + if (keyEvent.key == Key.Enter && keyEvent.type == KeyEventType.KeyDown && !keyEvent.isShiftPressed && !keyEvent.isCtrlPressed && !keyEvent.isMetaPressed && !isProcessing ) { - if (inputText.isNotBlank()) { - onSend(inputText) - textFieldState.edit { replace(0, length, "") } - } + doSend() true } else { false @@ -109,12 +113,7 @@ fun IdeaInputSection( // Bottom toolbar IdeaBottomToolbar( - onSendClick = { - if (inputText.isNotBlank()) { - onSend(inputText) - textFieldState.edit { replace(0, length, "") } - } - }, + onSendClick = doSend, sendEnabled = inputText.isNotBlank() && !isProcessing, isExecuting = isProcessing, onStopClick = onStop, From 72babb1a9a802aa9ec7c554705f2cb67760c1387 Mon Sep 17 00:00:00 2001 From: Fengda Huang Date: Sun, 30 Nov 2025 06:55:34 +0000 Subject: [PATCH 4/4] fix: resolve ClassCastException and duplicate key issues, replace emojis with icons - Fix ClassCastException in AnalysisAgent by validating SubAgent input before creating invocation - Fix duplicate key error in LazyColumn by adding unique ID to TimelineItem - Replace all emojis with Material Design icons from AutoDevComposeIcons - Tool icon (Build) - Error icon (Error) - Success/Failure icons (CheckCircle/Error) - Settings icon (Settings) - Remove emoji from recovery advice and confirmation messages Fixes: - java.lang.ClassCastException: LinkedHashMap cannot be cast to ContentHandlerContext - IllegalArgumentException: Key was already used in LazyColumn --- .../devti/gui/chat/ui/AutoDevInput.kt | 5 +- .../agent/orchestrator/ToolOrchestrator.kt | 19 +++++- .../devins/idea/renderer/JewelRenderer.kt | 36 +++++++---- .../devins/idea/toolwindow/IdeaAgentApp.kt | 61 ++++++++++++++----- 4 files changed, 91 insertions(+), 30 deletions(-) diff --git a/core/src/main/kotlin/cc/unitmesh/devti/gui/chat/ui/AutoDevInput.kt b/core/src/main/kotlin/cc/unitmesh/devti/gui/chat/ui/AutoDevInput.kt index 642cea20b8..26c9c0ef25 100644 --- a/core/src/main/kotlin/cc/unitmesh/devti/gui/chat/ui/AutoDevInput.kt +++ b/core/src/main/kotlin/cc/unitmesh/devti/gui/chat/ui/AutoDevInput.kt @@ -11,6 +11,7 @@ import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.editor.EditorModificationUtil import com.intellij.openapi.editor.actions.EnterAction import com.intellij.openapi.editor.actions.IncrementalFindAction @@ -181,7 +182,9 @@ class AutoDevInput( val id = UUID.randomUUID() val file = LightVirtualFile("AutoDevInput-$id", language, "") - val document = file.findDocument() ?: throw IllegalStateException("Can't create in-memory document") + val document = ReadAction.compute { + EditorFactory.getInstance().createDocument("") + } initializeDocumentListeners(document) setDocument(document) diff --git a/mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/orchestrator/ToolOrchestrator.kt b/mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/orchestrator/ToolOrchestrator.kt index 7fac165bc3..c2211123ca 100644 --- a/mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/orchestrator/ToolOrchestrator.kt +++ b/mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/orchestrator/ToolOrchestrator.kt @@ -585,13 +585,28 @@ class ToolOrchestrator( context: cc.unitmesh.agent.tool.ToolExecutionContext ): ToolResult { return try { - // Cast to ExecutableTool (all new tools should implement this) + // For SubAgent, we need to validate input first + if (tool is cc.unitmesh.agent.core.SubAgent<*, *>) { + @Suppress("UNCHECKED_CAST") + val subAgent = tool as cc.unitmesh.agent.core.SubAgent + + // Validate and convert Map to proper input type + val validatedInput = subAgent.validateInput(params) + + // Create invocation with validated input + val invocation = subAgent.createInvocation(validatedInput) + + // Execute the tool + return invocation.execute(context) + } + + // For other ExecutableTools, use params directly @Suppress("UNCHECKED_CAST") val executableTool = tool as? ExecutableTool ?: return ToolResult.Error("Tool ${tool.name} does not implement ExecutableTool interface") val invocation = executableTool.createInvocation(params) - + // Execute the tool invocation.execute(context) } catch (e: Exception) { diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt index 847c7bc78a..4e47dfaeee 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt @@ -71,13 +71,14 @@ class JewelRenderer : BaseRenderer() { val tasks: StateFlow> = _tasks.asStateFlow() // Data classes for timeline items - aligned with ComposeRenderer - sealed class TimelineItem(val timestamp: Long = System.currentTimeMillis()) { + sealed class TimelineItem(val timestamp: Long = System.currentTimeMillis(), val id: String = generateId()) { data class MessageItem( val role: MessageRole, val content: String, val tokenInfo: TokenInfo? = null, - val itemTimestamp: Long = System.currentTimeMillis() - ) : TimelineItem(itemTimestamp) + val itemTimestamp: Long = System.currentTimeMillis(), + val itemId: String = generateId() + ) : TimelineItem(itemTimestamp, itemId) /** * Combined tool call and result item - displays both in a single compact row. @@ -95,28 +96,37 @@ class JewelRenderer : BaseRenderer() { val output: String? = null, val fullOutput: String? = null, val executionTimeMs: Long? = null, - val itemTimestamp: Long = System.currentTimeMillis() - ) : TimelineItem(itemTimestamp) + val itemTimestamp: Long = System.currentTimeMillis(), + val itemId: String = generateId() + ) : TimelineItem(itemTimestamp, itemId) data class ErrorItem( val message: String, - val itemTimestamp: Long = System.currentTimeMillis() - ) : TimelineItem(itemTimestamp) + val itemTimestamp: Long = System.currentTimeMillis(), + val itemId: String = generateId() + ) : TimelineItem(itemTimestamp, itemId) data class TaskCompleteItem( val success: Boolean, val message: String, val iterations: Int, - val itemTimestamp: Long = System.currentTimeMillis() - ) : TimelineItem(itemTimestamp) + val itemTimestamp: Long = System.currentTimeMillis(), + val itemId: String = generateId() + ) : TimelineItem(itemTimestamp, itemId) data class TerminalOutputItem( val command: String, val output: String, val exitCode: Int, val executionTimeMs: Long, - val itemTimestamp: Long = System.currentTimeMillis() - ) : TimelineItem(itemTimestamp) + val itemTimestamp: Long = System.currentTimeMillis(), + val itemId: String = generateId() + ) : TimelineItem(itemTimestamp, itemId) + + companion object { + private var idCounter = 0L + fun generateId(): String = "${System.currentTimeMillis()}-${idCounter++}" + } } data class ToolCallInfo( @@ -386,7 +396,7 @@ class JewelRenderer : BaseRenderer() { addTimelineItem( TimelineItem.MessageItem( role = MessageRole.ASSISTANT, - content = "🔧 Recovery Advice:\n$recoveryAdvice" + content = "Recovery Advice:\n$recoveryAdvice" ) ) } @@ -395,7 +405,7 @@ class JewelRenderer : BaseRenderer() { addTimelineItem( TimelineItem.MessageItem( role = MessageRole.SYSTEM, - content = "⚠️ Confirmation required for tool: $toolName" + content = "Confirmation required for tool: $toolName" ) ) } diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt index 667af75c38..aaac4d9b3b 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt @@ -24,6 +24,7 @@ import cc.unitmesh.devins.idea.editor.IdeaInputTrigger import cc.unitmesh.devins.idea.renderer.JewelRenderer import cc.unitmesh.devins.idea.toolwindow.codereview.IdeaCodeReviewContent import cc.unitmesh.devins.idea.toolwindow.codereview.IdeaCodeReviewViewModel +import cc.unitmesh.devins.ui.compose.icons.AutoDevComposeIcons import cc.unitmesh.devins.ui.compose.theme.AutoDevColors import com.intellij.openapi.Disposable import com.intellij.openapi.editor.ex.EditorEx @@ -177,7 +178,7 @@ private fun TimelineContent( contentPadding = PaddingValues(8.dp), verticalArrangement = Arrangement.spacedBy(4.dp) ) { - items(timeline, key = { it.timestamp }) { item -> + items(timeline, key = { it.id }) { item -> TimelineItemView(item) } @@ -295,8 +296,14 @@ private fun ToolCallBubble(item: JewelRenderer.TimelineItem.ToolCallItem) { text = statusIcon, style = JewelTheme.defaultTextStyle.copy(color = statusColor) ) + Icon( + imageVector = AutoDevComposeIcons.Build, + contentDescription = "Tool", + modifier = Modifier.size(14.dp), + tint = JewelTheme.globalColors.text.normal + ) Text( - text = "🔧 ${item.toolName}", + text = item.toolName, style = JewelTheme.defaultTextStyle.copy(fontWeight = FontWeight.Bold) ) } @@ -337,12 +344,23 @@ private fun ErrorBubble(message: String) { .background(AutoDevColors.Red.c400.copy(alpha = 0.2f)) // Error background from design system .padding(8.dp) ) { - Text( - text = "❌ $message", - style = JewelTheme.defaultTextStyle.copy( - color = AutoDevColors.Red.c400 // Error text color from design system + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = AutoDevComposeIcons.Error, + contentDescription = "Error", + modifier = Modifier.size(16.dp), + tint = AutoDevColors.Red.c400 ) - ) + Text( + text = message, + style = JewelTheme.defaultTextStyle.copy( + color = AutoDevColors.Red.c400 // Error text color from design system + ) + ) + } } } } @@ -364,13 +382,23 @@ private fun TaskCompleteBubble(item: JewelRenderer.TimelineItem.TaskCompleteItem ) .padding(horizontal = 16.dp, vertical = 8.dp) ) { - val icon = if (item.success) "✅" else "❌" - Text( - text = "$icon ${item.message} (${item.iterations} iterations)", - style = JewelTheme.defaultTextStyle.copy( - fontWeight = FontWeight.Bold + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = if (item.success) AutoDevComposeIcons.CheckCircle else AutoDevComposeIcons.Error, + contentDescription = if (item.success) "Success" else "Failed", + modifier = Modifier.size(16.dp), + tint = if (item.success) AutoDevColors.Green.c400 else AutoDevColors.Red.c400 ) - ) + Text( + text = "${item.message} (${item.iterations} iterations)", + style = JewelTheme.defaultTextStyle.copy( + fontWeight = FontWeight.Bold + ) + ) + } } } } @@ -434,7 +462,12 @@ private fun AgentTabsHeader( Text("+", style = JewelTheme.defaultTextStyle.copy(fontWeight = FontWeight.Bold)) } IconButton(onClick = onSettings) { - Text("⚙", style = JewelTheme.defaultTextStyle) + Icon( + imageVector = AutoDevComposeIcons.Settings, + contentDescription = "Settings", + modifier = Modifier.size(16.dp), + tint = JewelTheme.globalColors.text.normal + ) } } }