diff --git a/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java b/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java index be51444615..cdf065008c 100644 --- a/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java +++ b/src/main/java/com/sourcegraph/cody/agent/CodyAgentClient.java @@ -3,15 +3,12 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; -import com.sourcegraph.cody.agent.protocol.ChatMessage; import com.sourcegraph.cody.agent.protocol.DebugMessage; -import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.function.Supplier; import org.eclipse.lsp4j.jsonrpc.services.JsonNotification; import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** Implementation of the client part of the Cody agent protocol. */ @@ -20,9 +17,7 @@ public class CodyAgentClient { private static final Logger logger = Logger.getInstance(CodyAgentClient.class); // Callback that is invoked when the agent sends a "chat/updateMessageInProgress" notification. - @Nullable public Consumer onChatUpdateMessageInProgress; - @Nullable public ConfigFeaturesObserver onConfigFeatures; - @NotNull public Runnable onFinishedProcessing = () -> {}; + @Nullable public Consumer onNewMessage; @Nullable public Editor editor; /** @@ -48,17 +43,6 @@ private CompletableFuture onEventThread(Supplier handler) { // Notifications // ============= - @JsonNotification("chat/updateMessageInProgress") - public void chatUpdateMessageInProgress(ChatMessage params) { - if (onChatUpdateMessageInProgress != null && params != null) { - ApplicationManager.getApplication() - .invokeLater(() -> onChatUpdateMessageInProgress.accept(params)); - } - if (params == null) { - onFinishedProcessing.run(); - } - } - @JsonNotification("debug/message") public void debugMessage(DebugMessage msg) { logger.warn(String.format("%s: %s", msg.getChannel(), msg.getMessage())); @@ -73,31 +57,14 @@ public CompletableFuture webviewCreate(WebviewCreateParams params) { @JsonNotification("webview/postMessage") public void webviewPostMessage(WebviewPostMessageParams params) { - if (params.getMessage().getType().equals("setConfigFeatures") - && params.getMessage().getConfigFeatures() != null) { - if (onConfigFeatures != null) { - onConfigFeatures.update(params.getMessage().getConfigFeatures()); - } - } - if (onChatUpdateMessageInProgress != null - && params.getMessage().getType().equals(ExtensionMessage.Type.TRANSCRIPT)) { - if (Boolean.FALSE.equals(params.getMessage().isMessageInProgress())) { - onFinishedProcessing.run(); - } else if (params.getMessage().getMessages() != null - && !params.getMessage().getMessages().isEmpty()) { - ApplicationManager.getApplication() - .invokeLater( - () -> - onChatUpdateMessageInProgress.accept( - Objects.requireNonNull(params.getMessage().getMessages()) - .get(params.getMessage().getMessages().size() - 1))); + ExtensionMessage extensionMessage = params.getMessage(); - } else { - logger.warn("webview/postMessage: no messages in transcript"); - } + if (onNewMessage != null + && extensionMessage.getType().equals(ExtensionMessage.Type.TRANSCRIPT)) { + ApplicationManager.getApplication().invokeLater(() -> onNewMessage.accept(params)); } else { - logger.warn("onChatUpdateMessageInProgress is null or message type is not transcript"); - logger.warn(String.format("webview/postMessage %s: %s", params.getId(), params.getMessage())); + logger.debug("onNewMessage is null or message type is not transcript"); + logger.debug(String.format("webview/postMessage %s: %s", params.getId(), extensionMessage)); } } } diff --git a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt index 3dc89affe0..6336b58a7c 100644 --- a/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt +++ b/src/main/kotlin/com/sourcegraph/cody/agent/CodyAgent.kt @@ -1,19 +1,15 @@ +package com.sourcegraph.cody.agent + import com.intellij.ide.plugins.PluginManagerCore -import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.extensions.PluginId import com.intellij.openapi.project.Project import com.intellij.openapi.util.SystemInfoRt import com.intellij.util.system.CpuArch -import com.sourcegraph.cody.agent.CodyAgentClient -import com.sourcegraph.cody.agent.CodyAgentException -import com.sourcegraph.cody.agent.CodyAgentServer -import com.sourcegraph.cody.agent.CurrentConfigFeatures import com.sourcegraph.cody.agent.protocol.* import com.sourcegraph.config.ConfigUtil -import java.io.File -import java.io.IOException -import java.io.PrintWriter +import java.io.* +import java.net.Socket import java.nio.file.* import java.util.* import java.util.concurrent.* @@ -29,7 +25,7 @@ private constructor( val client: CodyAgentClient, val server: CodyAgentServer, val launcher: Launcher, - private val agentProcess: Process, + private val connection: AgentConnection, private val listeningToJsonRpc: Future ) { @@ -38,7 +34,7 @@ private constructor( server.exit() logger.warn("Cody Agent shut down") listeningToJsonRpc.cancel(true) - agentProcess.destroyForcibly() + connection.close() } } @@ -46,20 +42,59 @@ private constructor( // NOTE(olafurpg): there are probably too many conditions below. We test multiple conditions // because we don't know 100% yet what exactly constitutes a "connected" state. Out of // abundance of caution, we check everything we can think of. - return agentProcess.isAlive && !listeningToJsonRpc.isDone && !listeningToJsonRpc.isCancelled + return connection.isConnected() && !listeningToJsonRpc.isDone && !listeningToJsonRpc.isCancelled + } + + /** Abstracts over the Process and Socket types to the extent we need it. */ + sealed class AgentConnection { + abstract fun isConnected(): Boolean + + abstract fun close() + + abstract fun getInputStream(): InputStream + + abstract fun getOutputStream(): OutputStream + + class ProcessConnection(val process: Process) : AgentConnection() { + override fun isConnected(): Boolean = process.isAlive + + override fun close() { + process.destroy() + } + + override fun getInputStream(): InputStream = process.inputStream + + override fun getOutputStream(): OutputStream = process.outputStream + } + + class SocketConnection(val socket: Socket) : AgentConnection() { + override fun isConnected(): Boolean = socket.isConnected && !socket.isClosed + + override fun close() { + socket.close() + } + + override fun getInputStream(): InputStream = socket.getInputStream() + + override fun getOutputStream(): OutputStream = socket.getOutputStream() + } } companion object { private val logger = Logger.getInstance(CodyAgent::class.java) private val PLUGIN_ID = PluginId.getId("com.sourcegraph.jetbrains") + private const val DEFAULT_AGENT_DEBUG_PORT = 3113 // Also defined in agent/src/cli/jsonrpc.ts @JvmField val executorService: ExecutorService = Executors.newCachedThreadPool() + private fun shouldConnectToDebugAgent() = System.getenv("CODY_AGENT_DEBUG_REMOTE") == "true" + + private fun shouldSpawnDebuggableAgent() = System.getenv("CODY_AGENT_DEBUG_INSPECT") == "true" + fun create(project: Project): CompletableFuture { try { - val agentProcess = startAgentProcess() + val conn = startAgentProcess() val client = CodyAgentClient() - client.onConfigFeatures = project.service() - val launcher = startAgentLauncher(agentProcess, client) + val launcher = startAgentLauncher(conn, client) val server = launcher.remoteProxy val listeningToJsonRpc = launcher.startListening() @@ -73,7 +108,7 @@ private constructor( .thenApply { info -> logger.info("Connected to Cody agent " + info.name) server.initialized() - CodyAgent(client, server, launcher, agentProcess, listeningToJsonRpc) + CodyAgent(client, server, launcher, conn, listeningToJsonRpc) } } catch (e: Exception) { logger.warn("Failed to send 'initialize' JSON-RPC request Cody agent", e) @@ -85,15 +120,22 @@ private constructor( } } - private fun startAgentProcess(): Process { - val binary = agentBinary() - logger.info("starting Cody agent " + binary.absolutePath) + private fun startAgentProcess(): AgentConnection { + if (shouldConnectToDebugAgent()) { + return connectToDebugAgent() + } val command: List = if (System.getenv("CODY_DIR") != null) { val script = File(System.getenv("CODY_DIR"), "agent/dist/index.js") logger.info("using Cody agent script " + script.absolutePath) - listOf("node", "--enable-source-maps", script.absolutePath) + if (shouldSpawnDebuggableAgent()) { + listOf("node", "--inspect", "--enable-source-maps", script.absolutePath) + } else { + listOf("node", "--enable-source-maps", script.absolutePath) + } } else { + val binary = agentBinary() + logger.info("starting Cody agent " + binary.absolutePath) listOf(binary.absolutePath) } @@ -114,38 +156,36 @@ private constructor( .start() // Redirect agent stderr into idea.log by buffering line by line into `logger.warn()` - // statements. Without this logic, the stderr output of the agent process is lost if the - // process - // fails to start for some reason. We use `logger.warn()` because the agent shouldn't print - // much - // normally (excluding a few noisy messages during initialization), it's mostly used to report - // unexpected errors. + // statements. Without this logic, the stderr output of the agent process is lost if + // the process fails to start for some reason. We use `logger.warn()` because the + // agent shouldn't print much normally (excluding a few noisy messages during + // initialization), it's mostly used to report unexpected errors. Thread { process.errorStream.bufferedReader().forEachLine { line -> logger.warn(line) } } .start() - return process + return AgentConnection.ProcessConnection(process) } @Throws(IOException::class, CodyAgentException::class) private fun startAgentLauncher( - agentProcess: Process, + process: AgentConnection, client: CodyAgentClient ): Launcher { return Launcher.Builder() .configureGson { gsonBuilder -> gsonBuilder // emit `null` instead of leaving fields undefined because Cody - // in VSC has - // many `=== null` checks that return false for undefined fields. + // VSC has many `=== null` checks that return false for undefined fields. .serializeNulls() .registerTypeAdapter(CompletionItemID::class.java, CompletionItemIDSerializer) .registerTypeAdapter(ContextFile::class.java, contextFileDeserializer) + .registerTypeAdapter(Speaker::class.java, SpeakerSerializer) } .setRemoteInterface(CodyAgentServer::class.java) .traceMessages(traceWriter()) .setExecutorService(executorService) - .setInput(agentProcess.inputStream) - .setOutput(agentProcess.outputStream) + .setInput(process.getInputStream()) + .setOutput(process.getOutputStream()) .setLocalService(client) .create() } @@ -163,6 +203,7 @@ private constructor( } private fun agentDirectory(): Path? { + // N.B. this is the default/production setting. CODY_DIR overrides it locally. val fromProperty = System.getProperty("cody-agent.directory", "") if (fromProperty.isNotEmpty()) { return Paths.get(fromProperty) @@ -212,5 +253,10 @@ private constructor( } return null } + + private fun connectToDebugAgent(): AgentConnection { + val port = System.getenv("CODY_AGENT_DEBUG_PORT")?.toInt() ?: DEFAULT_AGENT_DEBUG_PORT + return AgentConnection.SocketConnection(Socket("localhost", port)) + } } }