Skip to content

Commit 6635210

Browse files
authored
Merge pull request #23 from devchat-ai/asynchronous-prompt
Use coroutines to make prompt responsive
2 parents faab726 + 2158ffe commit 6635210

6 files changed

Lines changed: 81 additions & 42 deletions

File tree

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies {
1616
implementation("com.alibaba:fastjson:2.0.42")
1717
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.16.0")
1818
implementation(kotlin("stdlib-jdk8"))
19+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
1920
}
2021

2122
// Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin

src/main/kotlin/ai/devchat/cli/DevChatWrapper.kt

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,38 @@ import ai.devchat.common.Settings
66
import com.alibaba.fastjson.JSON
77
import com.alibaba.fastjson.JSONArray
88
import com.intellij.util.containers.addIfNotNull
9-
import java.io.BufferedReader
9+
import kotlinx.coroutines.*
1010
import java.io.IOException
1111

1212
private const val DEFAULT_LOG_MAX_COUNT = 10000
1313

14+
15+
private suspend fun Process.await(
16+
onOutput: (String) -> Unit,
17+
onError: (String) -> Unit
18+
): Int = coroutineScope {
19+
launch(Dispatchers.IO) {
20+
inputStream.bufferedReader().forEachLine { onOutput(it) }
21+
errorStream.bufferedReader().forEachLine { onError(it) }
22+
}
23+
val processExitCode = this@await.waitFor()
24+
processExitCode
25+
}
26+
27+
suspend fun executeCommand(
28+
command: List<String>,
29+
env: Map<String, String>,
30+
onOutputLine: (String) -> Unit,
31+
onErrorLine: (String) -> Unit
32+
): Int {
33+
val processBuilder = ProcessBuilder(command)
34+
env.forEach { (key, value) -> processBuilder.environment()[key] = value}
35+
val process = withContext(Dispatchers.IO) {
36+
processBuilder.start()
37+
}
38+
return process.await(onOutputLine, onErrorLine)
39+
}
40+
1441
class DevChatWrapper(
1542
private val command: String = DevChatPathUtil.devchatBinPath,
1643
private var apiBase: String? = null,
@@ -26,10 +53,8 @@ class DevChatWrapper(
2653
}
2754
}
2855

29-
private fun execCommand(commands: List<String>, callback: ((String) -> Unit)?): String? {
30-
val pb = ProcessBuilder(commands)
31-
val env = pb.environment()
32-
56+
private fun getEnv(): Map<String, String> {
57+
val env: MutableMap<String, String> = mutableMapOf()
3358
apiBase?.let {
3459
env["OPENAI_API_BASE"] = it
3560
Log.info("api_base: $it")
@@ -38,36 +63,50 @@ class DevChatWrapper(
3863
env["OPENAI_API_KEY"] = it
3964
Log.info("api_key: ${it.substring(0, 5)}...${it.substring(it.length - 4)}")
4065
}
66+
return env
67+
}
68+
69+
private fun execCommand(commands: List<String>): String {
70+
Log.info("Executing command: ${commands.joinToString(" ")}}")
4171
return try {
42-
Log.info("Executing command: ${commands.joinToString(" ")}}")
43-
val process = pb.start()
44-
val text = process.inputStream.bufferedReader().use { reader ->
45-
callback?.let {
46-
reader.forEachLine(it)
47-
""
48-
} ?: reader.readText()
72+
val outputLines: MutableList<String> = mutableListOf()
73+
val errorLines: MutableList<String> = mutableListOf()
74+
val exitCode = runBlocking {
75+
executeCommand(commands, getEnv(), outputLines::add, errorLines::add)
4976
}
50-
val errors = process.errorStream.bufferedReader().use(BufferedReader::readText)
51-
process.waitFor()
52-
val exitCode = process.exitValue()
77+
val errors = errorLines.joinToString("\n")
5378

5479
if (exitCode != 0) {
55-
Log.error("Failed to execute command: $commands Exit Code: $exitCode Error: $errors")
56-
throw RuntimeException(
57-
"Failed to execute command: $commands Exit Code: $exitCode Error: $errors"
58-
)
80+
throw RuntimeException("Command failure with exit Code: $exitCode, Errors: $errors")
5981
} else {
60-
text
82+
outputLines.joinToString("\n")
6183
}
6284
} catch (e: IOException) {
63-
Log.error("Failed to execute command: $commands")
64-
throw RuntimeException("Failed to execute command: $commands", e)
65-
} catch (e: InterruptedException) {
66-
Log.error("Failed to execute command: $commands")
85+
Log.error("Failed to execute command: $commands, Exception: $e")
6786
throw RuntimeException("Failed to execute command: $commands", e)
6887
}
6988
}
7089

90+
private fun execCommandAsync(
91+
commands: List<String>,
92+
onOutput: (String) -> Unit,
93+
onError: (String) -> Unit = Log::error
94+
): Job {
95+
Log.info("Executing command: ${commands.joinToString(" ")}}")
96+
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
97+
Log.error("Failed to execute command: $commands, Exception: $exception")
98+
throw RuntimeException("Failed to execute command: $commands", exception)
99+
}
100+
val cmdScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
101+
102+
return cmdScope.launch(exceptionHandler) {
103+
val exitCode = executeCommand(commands, getEnv(), onOutput, onError)
104+
if (exitCode != 0) {
105+
throw RuntimeException("Command failure with exit Code: $exitCode")
106+
}
107+
}
108+
}
109+
71110
val prompt: (MutableList<Pair<String, String?>>, String, ((String) -> Unit)?) -> Unit get() = {
72111
flags: MutableList<Pair<String, String?>>, message: String, callback: ((String) -> Unit)? ->
73112
flags.addAll(listOf("model" to currentModel, "" to message))
@@ -98,7 +137,7 @@ class DevChatWrapper(
98137
cmd.addIfNotNull(value)
99138
}
100139
return try {
101-
execCommand(cmd, callback)
140+
callback?.let { execCommandAsync(cmd, callback); "" } ?: execCommand(cmd)
102141
} catch (e: Exception) {
103142
Log.error("Failed to run command $cmd: ${e.message}")
104143
throw RuntimeException("Failed to run command $cmd", e)
@@ -114,7 +153,7 @@ class DevChatWrapper(
114153
cmd.addIfNotNull(value)
115154
}
116155
try {
117-
execCommand(cmd, callback)
156+
callback?.let { execCommandAsync(cmd, callback); "" } ?: execCommand(cmd)
118157
} catch (e: Exception) {
119158
Log.error("Failed to run command $cmd: ${e.message}")
120159
throw RuntimeException("Failed to run command $cmd", e)

src/main/kotlin/ai/devchat/common/Log.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
package ai.devchat.common
22

3-
import ai.devchat.cli.DevChatInstallationManager
43
import com.intellij.openapi.diagnostic.LogLevel
54
import com.intellij.openapi.diagnostic.Logger
65

76
object Log {
8-
private val LOG = Logger.getInstance(
9-
DevChatInstallationManager::class.java
10-
)
7+
private val LOG = Logger.getInstance("DevChat")
118
private const val PREFIX = "[DevChat] "
129
private fun setLevel(level: LogLevel) {
1310
LOG.setLevel(level)

src/main/kotlin/ai/devchat/devchat/handler/SendMessageRequestHandler.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class SendMessageRequestHandler(metadata: JSONObject?, payload: JSONObject?) : B
5454
response.update(line)
5555
promptCallback(response)
5656
}
57+
/* TODO: update messages cache with new one
5758
val currentTopic = ActiveConversation.topic ?: response.promptHash!!
5859
val newMessage = wrapper.logTopic(currentTopic, 1).getJSONObject(0)
5960
@@ -62,6 +63,7 @@ class SendMessageRequestHandler(metadata: JSONObject?, payload: JSONObject?) : B
6263
} else {
6364
ActiveConversation.reset(currentTopic, listOf(newMessage))
6465
}
66+
*/
6567
}
6668

6769
override fun except(exception: Exception) {
@@ -106,7 +108,7 @@ class SendMessageRequestHandler(metadata: JSONObject?, payload: JSONObject?) : B
106108
// Loop through the command names and check if message starts with it
107109
for (command in commandNames) {
108110
if (message.startsWith("/$command ")) {
109-
if (message.length > command!!.length + 2) {
111+
if (message.length > command.length + 2) {
110112
message = message.substring(command.length + 2) // +2 to take into account the '/' and the space ' '
111113
}
112114
runResult = wrapper.runCommand(listOf(command), null)

src/main/kotlin/ai/devchat/idea/DevChatToolWindow.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package ai.devchat.idea
22

3-
import ai.devchat.common.Log.error
4-
import ai.devchat.common.Log.info
5-
import ai.devchat.common.Log.setLevelInfo
3+
import ai.devchat.common.Log
64
import ai.devchat.devchat.DevChatActionHandler.Companion.instance
75
import com.intellij.openapi.editor.colors.EditorColors
86
import com.intellij.openapi.editor.colors.EditorColorsManager
@@ -40,12 +38,12 @@ internal class DevChatToolWindowContent(project: Project) {
4038
private val project: Project
4139

4240
init {
43-
setLevelInfo()
41+
Log.setLevelInfo()
4442
this.project = project
4543
content = JPanel(BorderLayout())
4644
// Check if JCEF is supported
4745
if (!JBCefApp.isSupported()) {
48-
error("JCEF is not supported.")
46+
Log.error("JCEF is not supported.")
4947
content.add(JLabel("JCEF is not supported", SwingConstants.CENTER))
5048
// TODO: 'return' is not allowed here
5149
// return
@@ -56,17 +54,17 @@ internal class DevChatToolWindowContent(project: Project) {
5654
// Read static files
5755
var htmlContent = readStaticFile("/static/main.html")
5856
if (htmlContent!!.isEmpty()) {
59-
error("main.html is missing.")
57+
Log.error("main.html is missing.")
6058
htmlContent = "<html><body><h1>Error: main.html is missing.</h1></body></html>"
6159
}
6260
var jsContent = readStaticFile("/static/main.js")
6361
if (jsContent!!.isEmpty()) {
64-
error("main.js is missing.")
62+
Log.error("main.js is missing.")
6563
jsContent = "console.log('Error: main.js not found')"
6664
}
6765
val HtmlWithCssContent = insertCSSToHTML(htmlContent)
6866
val HtmlWithJsContent = insertJStoHTML(HtmlWithCssContent, jsContent)
69-
info("main.html and main.js are loaded.")
67+
Log.info("main.html and main.js are loaded.")
7068

7169
// enable dev tools
7270
val myDevTools = jbCefBrowser.cefBrowser.devTools

src/main/kotlin/ai/devchat/idea/JSJavaBridge.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ai.devchat.idea
33
import ai.devchat.common.Log.info
44
import ai.devchat.devchat.ActionHandlerFactory
55
import com.alibaba.fastjson.JSON
6+
import com.intellij.openapi.application.ApplicationManager
67
import com.intellij.ui.jcef.JBCefBrowser
78
import com.intellij.ui.jcef.JBCefBrowserBase
89
import com.intellij.ui.jcef.JBCefJSQuery
@@ -12,10 +13,9 @@ import org.cef.handler.CefLoadHandlerAdapter
1213
import org.cef.network.CefRequest
1314

1415
class JSJavaBridge(private val jbCefBrowser: JBCefBrowser) {
15-
private val jsQuery: JBCefJSQuery
16+
private val jsQuery: JBCefJSQuery = JBCefJSQuery.create((jbCefBrowser as JBCefBrowserBase))
1617

1718
init {
18-
jsQuery = JBCefJSQuery.create((jbCefBrowser as JBCefBrowserBase))
1919
jsQuery.addHandler { arg: String -> callJava(arg) }
2020
}
2121

@@ -33,7 +33,9 @@ class JSJavaBridge(private val jbCefBrowser: JBCefBrowser) {
3333
val payload = jsonObject.getJSONObject("payload")
3434
info("Got action: $action")
3535
val actionHandler = ActionHandlerFactory().createActionHandler(action, metadata, payload)
36-
actionHandler.executeAction()
36+
ApplicationManager.getApplication().invokeLater {
37+
actionHandler.executeAction()
38+
}
3739
return JBCefJSQuery.Response("ignore me")
3840
}
3941

0 commit comments

Comments
 (0)