package cc.unitmesh.devins.idea.toolwindow.remote import cc.unitmesh.agent.RemoteAgentEvent import cc.unitmesh.devins.idea.renderer.JewelRenderer import cc.unitmesh.config.ConfigManager import com.intellij.openapi.Disposable import com.intellij.openapi.project.Project import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow /** * ViewModel for Remote Agent in IntelliJ IDEA plugin. * * Connects to mpp-server and streams agent execution events, * forwarding them to JewelRenderer for UI rendering. * * This is adapted from mpp-ui's RemoteCodingAgentViewModel. */ class IdeaRemoteAgentViewModel( val project: Project, private val coroutineScope: CoroutineScope, serverUrl: String = "http://localhost:8080", private val useServerConfig: Boolean = false ) : Disposable { private var _serverUrl = serverUrl val serverUrl: String get() = _serverUrl private var client = IdeaRemoteAgentClient(_serverUrl) val renderer = JewelRenderer() private val _isExecuting = MutableStateFlow(false) val isExecuting: StateFlow = _isExecuting.asStateFlow() private val _isConnected = MutableStateFlow(false) val isConnected: StateFlow = _isConnected.asStateFlow() private val _connectionError = MutableStateFlow(null) val connectionError: StateFlow = _connectionError.asStateFlow() private val _availableProjects = MutableStateFlow>(emptyList()) val availableProjects: StateFlow> = _availableProjects.asStateFlow() private var currentExecutionJob: Job? = null /** * Update server URL and recreate client */ fun updateServerUrl(newUrl: String) { if (newUrl != _serverUrl) { _serverUrl = newUrl client.close() client = IdeaRemoteAgentClient(_serverUrl) _isConnected.value = false _connectionError.value = null _availableProjects.value = emptyList() } } /** * Check connection to server * Uses Dispatchers.IO to avoid blocking EDT during network calls */ fun checkConnection() { coroutineScope.launch(Dispatchers.IO) { try { val health = client.healthCheck() _isConnected.value = health.status == "ok" _connectionError.value = null if (_isConnected.value) { val projectList = client.getProjects() _availableProjects.value = projectList.projects } } catch (e: Exception) { _isConnected.value = false _connectionError.value = e.message ?: "Failed to connect to server" } } } /** * Execute a task on the remote server */ fun executeTask(projectId: String, task: String, gitUrl: String = "") { if (_isExecuting.value) { println("Agent is already executing") return } if (!_isConnected.value) { renderer.renderError("Not connected to server. Please check server URL.") return } _isExecuting.value = true renderer.clearError() renderer.addUserMessage(task) // Use Dispatchers.IO to avoid blocking EDT during network streaming currentExecutionJob = coroutineScope.launch(Dispatchers.IO) { try { val llmConfig = if (!useServerConfig) { val config = ConfigManager.load() val activeConfig = config.getActiveModelConfig() if (activeConfig == null) { renderer.renderError("No active LLM configuration found. Please configure your model first.") _isExecuting.value = false return@launch } LLMConfig( provider = activeConfig.provider.name, modelName = activeConfig.modelName, apiKey = activeConfig.apiKey ?: "", baseUrl = activeConfig.baseUrl ) } else { null } val request = buildRequest(projectId, task, gitUrl, llmConfig) client.executeStream(request).collect { event -> handleRemoteEvent(event) if (event is RemoteAgentEvent.Complete) { _isExecuting.value = false currentExecutionJob = null } } } catch (e: CancellationException) { renderer.forceStop() renderer.renderError("Task cancelled by user") _isExecuting.value = false currentExecutionJob = null } catch (e: Exception) { renderer.renderError(e.message ?: "Unknown error") _isExecuting.value = false currentExecutionJob = null } } } private fun buildRequest( projectId: String, task: String, gitUrl: String, llmConfig: LLMConfig? ): RemoteAgentRequest { return if (gitUrl.isNotBlank()) { RemoteAgentRequest( projectId = extractProjectIdFromUrl(gitUrl) ?: "temp-project", task = task, llmConfig = llmConfig, gitUrl = gitUrl ) } else { val isGitUrl = projectId.startsWith("http://") || projectId.startsWith("https://") || projectId.startsWith("git@") if (isGitUrl) { RemoteAgentRequest( projectId = extractProjectIdFromUrl(projectId) ?: "temp-project", task = task, llmConfig = llmConfig, gitUrl = projectId ) } else { RemoteAgentRequest( projectId = projectId, task = task, llmConfig = llmConfig ) } } } /** * Extract project ID from a Git URL, handling trailing slashes and empty segments. */ private fun extractProjectIdFromUrl(url: String): String? { return url.trimEnd('/') .split('/') .lastOrNull { it.isNotBlank() } ?.removeSuffix(".git") ?.ifBlank { null } } /** * Handle remote events and forward to JewelRenderer */ private fun handleRemoteEvent(event: RemoteAgentEvent) { when (event) { is RemoteAgentEvent.CloneProgress -> { if (event.progress != null) { renderer.renderLLMResponseStart() renderer.renderLLMResponseChunk("📦 Cloning repository: ${event.stage} (${event.progress}%)") renderer.renderLLMResponseEnd() } } is RemoteAgentEvent.CloneLog -> { if (!event.isError && (event.message.contains("✓") || event.message.contains("ready"))) { renderer.renderLLMResponseStart() renderer.renderLLMResponseChunk(event.message) renderer.renderLLMResponseEnd() } else if (event.isError) { renderer.renderError(event.message) } } is RemoteAgentEvent.Iteration -> { renderer.renderIterationHeader(event.current, event.max) } is RemoteAgentEvent.LLMChunk -> { if (!renderer.isProcessing.value) { renderer.renderLLMResponseStart() } renderer.renderLLMResponseChunk(event.chunk) } is RemoteAgentEvent.ToolCall -> { if (renderer.isProcessing.value) { renderer.renderLLMResponseEnd() } renderer.renderToolCall(event.toolName, event.params) } is RemoteAgentEvent.ToolResult -> { renderer.renderToolResult( toolName = event.toolName, success = event.success, output = event.output, fullOutput = event.output, metadata = emptyMap() ) } is RemoteAgentEvent.Error -> { renderer.renderError(event.message) } is RemoteAgentEvent.Complete -> { if (renderer.isProcessing.value) { renderer.renderLLMResponseEnd() } renderer.renderFinalResult(event.success, event.message, event.iterations) } } } /** * Cancel current task */ fun cancelTask() { if (_isExecuting.value && currentExecutionJob != null) { currentExecutionJob?.cancel("Task cancelled by user") currentExecutionJob = null _isExecuting.value = false } } /** * Clear chat history */ fun clearHistory() { renderer.clearTimeline() } /** * Clear error state */ fun clearError() { renderer.clearError() _connectionError.value = null } override fun dispose() { currentExecutionJob?.cancel() client.close() } }