|
| 1 | +package cc.unitmesh.devins.idea.renderer |
| 2 | + |
| 3 | +import cc.unitmesh.agent.render.BaseRenderer |
| 4 | +import cc.unitmesh.llm.compression.TokenInfo |
| 5 | +import kotlinx.coroutines.flow.MutableStateFlow |
| 6 | +import kotlinx.coroutines.flow.StateFlow |
| 7 | +import kotlinx.coroutines.flow.asStateFlow |
| 8 | +import kotlinx.coroutines.flow.update |
| 9 | + |
| 10 | +/** |
| 11 | + * Jewel-compatible Renderer for IntelliJ IDEA plugin. |
| 12 | + * |
| 13 | + * Uses Kotlin StateFlow instead of Compose mutableStateOf to avoid |
| 14 | + * ClassLoader conflicts with IntelliJ's bundled Compose runtime. |
| 15 | + * |
| 16 | + * Implements CodingAgentRenderer interface from mpp-core. |
| 17 | + */ |
| 18 | +class JewelRenderer : BaseRenderer() { |
| 19 | + |
| 20 | + // Timeline of all events (messages, tool calls, results) |
| 21 | + private val _timeline = MutableStateFlow<List<TimelineItem>>(emptyList()) |
| 22 | + val timeline: StateFlow<List<TimelineItem>> = _timeline.asStateFlow() |
| 23 | + |
| 24 | + // Current streaming output from LLM |
| 25 | + private val _currentStreamingOutput = MutableStateFlow("") |
| 26 | + val currentStreamingOutput: StateFlow<String> = _currentStreamingOutput.asStateFlow() |
| 27 | + |
| 28 | + // Processing state |
| 29 | + private val _isProcessing = MutableStateFlow(false) |
| 30 | + val isProcessing: StateFlow<Boolean> = _isProcessing.asStateFlow() |
| 31 | + |
| 32 | + // Iteration tracking |
| 33 | + private val _currentIteration = MutableStateFlow(0) |
| 34 | + val currentIteration: StateFlow<Int> = _currentIteration.asStateFlow() |
| 35 | + |
| 36 | + private val _maxIterations = MutableStateFlow(100) |
| 37 | + val maxIterations: StateFlow<Int> = _maxIterations.asStateFlow() |
| 38 | + |
| 39 | + // Current active tool call |
| 40 | + private val _currentToolCall = MutableStateFlow<ToolCallInfo?>(null) |
| 41 | + val currentToolCall: StateFlow<ToolCallInfo?> = _currentToolCall.asStateFlow() |
| 42 | + |
| 43 | + // Error state |
| 44 | + private val _errorMessage = MutableStateFlow<String?>(null) |
| 45 | + val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow() |
| 46 | + |
| 47 | + // Task completion state |
| 48 | + private val _taskCompleted = MutableStateFlow(false) |
| 49 | + val taskCompleted: StateFlow<Boolean> = _taskCompleted.asStateFlow() |
| 50 | + |
| 51 | + // Token tracking |
| 52 | + private val _totalTokenInfo = MutableStateFlow(TokenInfo()) |
| 53 | + val totalTokenInfo: StateFlow<TokenInfo> = _totalTokenInfo.asStateFlow() |
| 54 | + |
| 55 | + // Execution timing |
| 56 | + private var executionStartTime = 0L |
| 57 | + |
| 58 | + // Data classes for timeline items |
| 59 | + sealed class TimelineItem(val timestamp: Long = System.currentTimeMillis()) { |
| 60 | + data class MessageItem( |
| 61 | + val role: MessageRole, |
| 62 | + val content: String, |
| 63 | + val tokenInfo: TokenInfo? = null, |
| 64 | + val itemTimestamp: Long = System.currentTimeMillis() |
| 65 | + ) : TimelineItem(itemTimestamp) |
| 66 | + |
| 67 | + data class ToolCallItem( |
| 68 | + val toolName: String, |
| 69 | + val params: String, |
| 70 | + val success: Boolean? = null, |
| 71 | + val output: String? = null, |
| 72 | + val executionTimeMs: Long? = null, |
| 73 | + val itemTimestamp: Long = System.currentTimeMillis() |
| 74 | + ) : TimelineItem(itemTimestamp) |
| 75 | + |
| 76 | + data class ErrorItem( |
| 77 | + val message: String, |
| 78 | + val itemTimestamp: Long = System.currentTimeMillis() |
| 79 | + ) : TimelineItem(itemTimestamp) |
| 80 | + |
| 81 | + data class TaskCompleteItem( |
| 82 | + val success: Boolean, |
| 83 | + val message: String, |
| 84 | + val iterations: Int, |
| 85 | + val itemTimestamp: Long = System.currentTimeMillis() |
| 86 | + ) : TimelineItem(itemTimestamp) |
| 87 | + } |
| 88 | + |
| 89 | + data class ToolCallInfo( |
| 90 | + val toolName: String, |
| 91 | + val params: String |
| 92 | + ) |
| 93 | + |
| 94 | + enum class MessageRole { |
| 95 | + USER, ASSISTANT, SYSTEM |
| 96 | + } |
| 97 | + |
| 98 | + // BaseRenderer implementation |
| 99 | + |
| 100 | + override fun renderIterationHeader(current: Int, max: Int) { |
| 101 | + _currentIteration.value = current |
| 102 | + _maxIterations.value = max |
| 103 | + } |
| 104 | + |
| 105 | + override fun renderLLMResponseStart() { |
| 106 | + super.renderLLMResponseStart() |
| 107 | + _currentStreamingOutput.value = "" |
| 108 | + _isProcessing.value = true |
| 109 | + |
| 110 | + if (executionStartTime == 0L) { |
| 111 | + executionStartTime = System.currentTimeMillis() |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + override fun renderLLMResponseChunk(chunk: String) { |
| 116 | + reasoningBuffer.append(chunk) |
| 117 | + |
| 118 | + // Wait for more content if we detect an incomplete devin block |
| 119 | + if (hasIncompleteDevinBlock(reasoningBuffer.toString())) { |
| 120 | + return |
| 121 | + } |
| 122 | + |
| 123 | + // Filter devin blocks and output clean content |
| 124 | + val processedContent = filterDevinBlocks(reasoningBuffer.toString()) |
| 125 | + val cleanContent = cleanNewlines(processedContent) |
| 126 | + |
| 127 | + _currentStreamingOutput.value = cleanContent |
| 128 | + } |
| 129 | + |
| 130 | + override fun renderLLMResponseEnd() { |
| 131 | + super.renderLLMResponseEnd() |
| 132 | + |
| 133 | + val content = _currentStreamingOutput.value.trim() |
| 134 | + if (content.isNotEmpty()) { |
| 135 | + addTimelineItem( |
| 136 | + TimelineItem.MessageItem( |
| 137 | + role = MessageRole.ASSISTANT, |
| 138 | + content = content, |
| 139 | + tokenInfo = _totalTokenInfo.value |
| 140 | + ) |
| 141 | + ) |
| 142 | + } |
| 143 | + |
| 144 | + _currentStreamingOutput.value = "" |
| 145 | + _isProcessing.value = false |
| 146 | + } |
| 147 | + |
| 148 | + override fun renderToolCall(toolName: String, paramsStr: String) { |
| 149 | + _currentToolCall.value = ToolCallInfo(toolName, paramsStr) |
| 150 | + |
| 151 | + addTimelineItem( |
| 152 | + TimelineItem.ToolCallItem( |
| 153 | + toolName = toolName, |
| 154 | + params = paramsStr |
| 155 | + ) |
| 156 | + ) |
| 157 | + } |
| 158 | + |
| 159 | + override fun renderToolResult( |
| 160 | + toolName: String, |
| 161 | + success: Boolean, |
| 162 | + output: String?, |
| 163 | + fullOutput: String?, |
| 164 | + metadata: Map<String, String> |
| 165 | + ) { |
| 166 | + _currentToolCall.value = null |
| 167 | + |
| 168 | + // Update the last tool call item with result |
| 169 | + _timeline.update { items -> |
| 170 | + items.mapIndexed { index, item -> |
| 171 | + if (index == items.lastIndex && item is TimelineItem.ToolCallItem && item.toolName == toolName) { |
| 172 | + item.copy( |
| 173 | + success = success, |
| 174 | + output = output ?: fullOutput |
| 175 | + ) |
| 176 | + } else { |
| 177 | + item |
| 178 | + } |
| 179 | + } |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + override fun renderTaskComplete() { |
| 184 | + _taskCompleted.value = true |
| 185 | + } |
| 186 | + |
| 187 | + override fun renderFinalResult(success: Boolean, message: String, iterations: Int) { |
| 188 | + addTimelineItem( |
| 189 | + TimelineItem.TaskCompleteItem( |
| 190 | + success = success, |
| 191 | + message = message, |
| 192 | + iterations = iterations |
| 193 | + ) |
| 194 | + ) |
| 195 | + _isProcessing.value = false |
| 196 | + executionStartTime = 0L |
| 197 | + } |
| 198 | + |
| 199 | + override fun renderError(message: String) { |
| 200 | + _errorMessage.value = message |
| 201 | + addTimelineItem(TimelineItem.ErrorItem(message)) |
| 202 | + } |
| 203 | + |
| 204 | + override fun renderRepeatWarning(toolName: String, count: Int) { |
| 205 | + val warning = "Tool '$toolName' called repeatedly ($count times)" |
| 206 | + addTimelineItem(TimelineItem.ErrorItem(warning)) |
| 207 | + } |
| 208 | + |
| 209 | + override fun renderRecoveryAdvice(recoveryAdvice: String) { |
| 210 | + addTimelineItem( |
| 211 | + TimelineItem.MessageItem( |
| 212 | + role = MessageRole.ASSISTANT, |
| 213 | + content = "🔧 Recovery Advice:\n$recoveryAdvice" |
| 214 | + ) |
| 215 | + ) |
| 216 | + } |
| 217 | + |
| 218 | + override fun renderUserConfirmationRequest(toolName: String, params: Map<String, Any>) { |
| 219 | + // For now, just render as a message |
| 220 | + addTimelineItem( |
| 221 | + TimelineItem.MessageItem( |
| 222 | + role = MessageRole.SYSTEM, |
| 223 | + content = "⚠️ Confirmation required for tool: $toolName" |
| 224 | + ) |
| 225 | + ) |
| 226 | + } |
| 227 | + |
| 228 | + override fun updateTokenInfo(tokenInfo: TokenInfo) { |
| 229 | + _totalTokenInfo.value = tokenInfo |
| 230 | + } |
| 231 | + |
| 232 | + // Public methods for UI interaction |
| 233 | + |
| 234 | + fun addUserMessage(content: String) { |
| 235 | + addTimelineItem( |
| 236 | + TimelineItem.MessageItem( |
| 237 | + role = MessageRole.USER, |
| 238 | + content = content |
| 239 | + ) |
| 240 | + ) |
| 241 | + } |
| 242 | + |
| 243 | + fun clearTimeline() { |
| 244 | + _timeline.value = emptyList() |
| 245 | + _currentStreamingOutput.value = "" |
| 246 | + _isProcessing.value = false |
| 247 | + _currentIteration.value = 0 |
| 248 | + _errorMessage.value = null |
| 249 | + _taskCompleted.value = false |
| 250 | + _totalTokenInfo.value = TokenInfo() |
| 251 | + executionStartTime = 0L |
| 252 | + } |
| 253 | + |
| 254 | + fun reset() { |
| 255 | + clearTimeline() |
| 256 | + } |
| 257 | + |
| 258 | + private fun addTimelineItem(item: TimelineItem) { |
| 259 | + _timeline.update { it + item } |
| 260 | + } |
| 261 | +} |
| 262 | + |
0 commit comments