Skip to content

Commit

Permalink
feat(intellij): add option to specify node binary; improve docs. (#529)
Browse files Browse the repository at this point in the history
  • Loading branch information
icycodes authored Oct 10, 2023
1 parent 6dbb712 commit fceb8c9
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 169 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.ui.Messages
import com.tabbyml.intellijtabby.agent.AgentService
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import kotlinx.coroutines.launch
import java.net.URL

class CheckIssueDetail : AnAction() {
private val logger = Logger.getInstance(CheckIssueDetail::class.java)
Expand All @@ -21,37 +23,43 @@ class CheckIssueDetail : AnAction() {
agentService.scope.launch {
val detail = agentService.getCurrentIssueDetail() ?: return@launch
val serverHealthState = agentService.getServerHealthState()
logger.info("Show issue detail: $detail, $serverHealthState")
val settingsState = service<ApplicationSettingsState>().state.value
logger.info("Show issue detail: $detail, $serverHealthState, $settingsState")
val title = when (detail["name"]) {
"slowCompletionResponseTime" -> "Completion Requests Appear to Take Too Much Time"
"highCompletionTimeoutRate" -> "Most Completion Requests Timed Out"
else -> return@launch
}
val message = buildDetailMessage(detail, serverHealthState)
val message = buildDetailMessage(detail, serverHealthState, settingsState)
invokeLater {
val result = Messages.showOkCancelDialog(message, title, "Dismiss", "Supported Models", Messages.getInformationIcon())
if (result == Messages.CANCEL) {
val result =
Messages.showOkCancelDialog(message, title, "Supported Models", "Dismiss", Messages.getInformationIcon())
if (result == Messages.OK) {
BrowserUtil.browse("https://tabby.tabbyml.com/docs/models/")
}
}
}
}

private fun buildDetailMessage(detail: Map<String, Any>, serverHealthState: Map<String, Any>?): String {
private fun buildDetailMessage(
detail: Map<String, Any>,
serverHealthState: Map<String, Any>?,
settingsState: ApplicationSettingsState.State
): String {
val stats = detail["completionResponseStats"] as Map<*, *>?
val statsMessages = when (detail["name"]) {
"slowCompletionResponseTime" -> if (stats != null && stats["responses"] is Number && stats["averageResponseTime"] is Number) {
val response = (stats["responses"] as Number).toInt()
val averageResponseTime = (stats["averageResponseTime"] as Number).toInt()
"The average response time of recent $response completion requests is $averageResponseTime ms.\n\n"
"The average response time of recent $response completion requests is $averageResponseTime ms."
} else {
""
}

"highCompletionTimeoutRate" -> if (stats != null && stats["total"] is Number && stats["timeouts"] is Number) {
val timeout = (stats["timeouts"] as Number).toInt()
val total = (stats["total"] as Number).toInt()
"$timeout of $total completion requests timed out.\n\n"
"$timeout of $total completion requests timed out."
} else {
""
}
Expand All @@ -63,27 +71,35 @@ class CheckIssueDetail : AnAction() {
val model = serverHealthState?.get("model") as String? ?: ""
val helpMessageForRunningLargeModelOnCPU = if (device == "cpu" && model.endsWith("B")) {
"""
Your Tabby server is running model $model on CPU.
Your Tabby server is running model <i>$model</i> on CPU.
This model is too large to run on CPU, please try a smaller model or switch to GPU.
You can find supported model list in online documents.
"""
""".trimIndent()
} else {
""
}
var helpMessage = ""
var commonHelpMessage = ""
val host = URL(settingsState.serverEndpoint).host
if (helpMessageForRunningLargeModelOnCPU.isEmpty()) {
commonHelpMessage += "<li>The running model <i>$model</i> is too large to run on your Tabby server.<br/>"
commonHelpMessage += "Please try a smaller model. You can find supported model list in online documents.</li>"
}
if (!(host == "localhost" || host == "127.0.0.1")) {
commonHelpMessage += "<li>A poor network connection. Please check your network and proxy settings.</li>"
commonHelpMessage += "<li>Server overload. Please contact your Tabby server administrator for assistance.</li>"
}

var helpMessage: String
if (helpMessageForRunningLargeModelOnCPU.isNotEmpty()) {
helpMessage += helpMessageForRunningLargeModelOnCPU + "\n\n"
helpMessage += "Other possible causes of this issue are: \n"
helpMessage = "$helpMessageForRunningLargeModelOnCPU<br/>"
if (commonHelpMessage.isNotEmpty()) {
helpMessage += "<br/>Other possible causes of this issue: <br/><ul>$commonHelpMessage</ul>"
}
} else {
helpMessage += "Possible causes of this issue are: \n";
}
helpMessage += " - A poor network connection. Please check your network and proxy settings.\n";
helpMessage += " - Server overload. Please contact your Tabby server administrator for assistance.\n";
if (helpMessageForRunningLargeModelOnCPU.isEmpty()) {
helpMessage += " - The running model $model is too large to run on your Tabby server. ";
helpMessage += "Please try a smaller model. You can find supported model list in online documents.\n";
// commonHelpMessage should not be empty here
helpMessage = "Possible causes of this issue: <br/><ul>$commonHelpMessage</ul>"
}
return statsMessages + helpMessage
return "<html>$statsMessages<br/><br/>$helpMessage</html>"
}

override fun update(e: AnActionEvent) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.tabbyml.intellijtabby.actions

import com.intellij.ide.BrowserUtil
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent

class OpenOnlineDocs: AnAction() {
override fun actionPerformed(e: AnActionEvent) {
BrowserUtil.browse("https://tabby.tabbyml.com/docs/extensions/")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@ import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessOutputTypes
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.util.Key
import com.intellij.util.EnvironmentUtil
import com.intellij.util.io.BaseOutputReader
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.io.OutputStreamWriter

Expand All @@ -46,27 +49,21 @@ class Agent : ProcessAdapter() {

open class AgentException(message: String) : Exception(message)

fun open() {
logger.info("Environment variables: PATH: ${EnvironmentUtil.getValue("PATH")}")

val node = PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("node")
if (node?.exists() == true) {
logger.info("Node bin path: ${node.absolutePath}")
} else {
throw AgentException("Node bin not found. Please install Node.js v16+ and add bin path to system environment variable PATH, then restart IDE.")
}
open class NodeBinaryException(message: String) : AgentException(
message = "$message Please install Node.js version >= 18.0, set the binary path in Tabby plugin settings or add bin path to system environment variable PATH, then restart IDE."
)

checkNodeVersion(node.absolutePath)
open class NodeBinaryNotFoundException : NodeBinaryException(
message = "Cannot find Node binary."
)

val script =
PluginManagerCore.getPlugin(PluginId.getId("com.tabbyml.intellij-tabby"))?.pluginPath?.resolve("node_scripts/tabby-agent.js")
?.toFile()
if (script?.exists() == true) {
logger.info("Node script path: ${script.absolutePath}")
} else {
throw AgentException("Node script not found. Please reinstall Tabby plugin.")
}
open class NodeBinaryInvalidVersionException(version: String) : NodeBinaryException(
message = "Node version is too old: $version."
)

fun open() {
val node = getNodeBinary()
val script = getNodeScript()
val cmd = GeneralCommandLine(node.absolutePath, script.absolutePath)
process = object : KillableProcessHandler(cmd) {
override fun readerOptions(): BaseOutputReader.Options {
Expand All @@ -78,25 +75,56 @@ class Agent : ProcessAdapter() {
streamWriter = process.processInput.writer()
}

private fun checkNodeVersion(node: String) {
private fun getNodeBinary(): File {
val settings = service<ApplicationSettingsState>()
val node = if (settings.nodeBinary.isNotBlank()) {
val path = settings.nodeBinary.replaceFirst(Regex("^~"), System.getProperty("user.home"))
File(path)
} else {
logger.info("Environment variables: PATH: ${EnvironmentUtil.getValue("PATH")}")
PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("node")
}

if (node?.exists() == true) {
logger.info("Node binary path: ${node.absolutePath}")
checkNodeVersion(node)
return node
} else {
throw NodeBinaryNotFoundException()
}
}

private fun checkNodeVersion(node: File) {
try {
val process = GeneralCommandLine(node, "--version").createProcess()
val process = GeneralCommandLine(node.absolutePath, "--version").createProcess()
val version = BufferedReader(InputStreamReader(process.inputStream)).readLine()
val regResult = Regex("v([0-9]+)\\.([0-9]+)\\.([0-9]+)").find(version)
if (regResult != null && regResult.groupValues[1].toInt() >= 18) {
return
} else {
throw AgentException("Node version is too old: $version. Please install Node.js v18+ and add bin path to system environment variable PATH, then restart IDE.")
throw NodeBinaryInvalidVersionException(version)
}
} catch (e: Exception) {
if (e is AgentException) {
throw e
} else {
throw AgentException("Failed to check node version: $e. Please check your node installation.")
throw AgentException("Failed to check node version: $e.")
}
}
}

private fun getNodeScript(): File {
val script =
PluginManagerCore.getPlugin(PluginId.getId("com.tabbyml.intellij-tabby"))?.pluginPath?.resolve("node_scripts/tabby-agent.js")
?.toFile()
if (script?.exists() == true) {
logger.info("Node script path: ${script.absolutePath}")
return script
} else {
throw AgentException("Node script not found. Please reinstall Tabby plugin.")
}
}

data class Config(
val server: Server? = null,
val completion: Completion? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import com.tabbyml.intellijtabby.usage.AnonymousUsageLogger
import io.ktor.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -62,9 +61,7 @@ class AgentService : Disposable {

init {
val settings = service<ApplicationSettingsState>()
val anonymousUsageLogger = service<AnonymousUsageLogger>()
scope.launch {

val config = createAgentConfig(settings.data)
val clientProperties = createClientProperties(settings.data)
try {
Expand All @@ -75,18 +72,14 @@ class AgentService : Disposable {
} catch (e: Exception) {
initResultFlow.value = false
logger.warn("Agent init failed: $e")
anonymousUsageLogger.event(
"IntelliJInitFailed", mapOf(
"client" to clientProperties.session["client"] as String, "error" to e.stackTraceToString()
)
)

val notification = Notification(
"com.tabbyml.intellijtabby.notification.warning",
"Tabby initialization failed",
"${e.message}",
NotificationType.ERROR,
)
// FIXME: Add action to open FAQ page to help user set up nodejs.
notification.addAction(ActionManager.getInstance().getAction("Tabby.OpenOnlineDocs"))
invokeLater {
initFailedNotification?.expire()
initFailedNotification = notification
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,23 @@ class ApplicationConfigurable : Configurable {
val settings = service<ApplicationSettingsState>()
return settingsPanel.completionTriggerMode != settings.completionTriggerMode
|| settingsPanel.serverEndpoint != settings.serverEndpoint
|| settingsPanel.nodeBinary != settings.nodeBinary
|| settingsPanel.isAnonymousUsageTrackingDisabled != settings.isAnonymousUsageTrackingDisabled
}

override fun apply() {
val settings = service<ApplicationSettingsState>()
settings.completionTriggerMode = settingsPanel.completionTriggerMode
settings.serverEndpoint = settingsPanel.serverEndpoint
settings.nodeBinary = settingsPanel.nodeBinary
settings.isAnonymousUsageTrackingDisabled = settingsPanel.isAnonymousUsageTrackingDisabled
}

override fun reset() {
val settings = service<ApplicationSettingsState>()
settingsPanel.completionTriggerMode = settings.completionTriggerMode
settingsPanel.serverEndpoint = settings.serverEndpoint
settingsPanel.nodeBinary = settings.nodeBinary
settingsPanel.isAnonymousUsageTrackingDisabled = settings.isAnonymousUsageTrackingDisabled
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,26 @@ class ApplicationSettingsPanel {
"""
<html>
A http or https URL of Tabby server endpoint.<br/>
If leave empty, server endpoint config in <i>~/.tabby-client/agent/config.toml</i> will be used<br/>
If leave empty, server endpoint config in <i>~/.tabby-client/agent/config.toml</i> will be used.<br/>
Default to <i>http://localhost:8080</i>.
</html>
""".trimIndent()
)
.panel

private val nodeBinaryTextField = JBTextField()
private val nodeBinaryPanel = FormBuilder.createFormBuilder()
.addComponent(nodeBinaryTextField)
.addTooltip(
"""
<html>
Path to the Node binary for running the Tabby agent. The Node version must be >= 18.0.<br/>
If left empty, Tabby will attempt to find the Node binary in the <i>PATH</i> environment variable.<br/>
</html>
""".trimIndent()
)
.panel

private val completionTriggerModeAutomaticRadioButton = JBRadioButton("Automatic")
private val completionTriggerModeManualRadioButton = JBRadioButton("Manual")
private val completionTriggerModeRadioGroup = ButtonGroup().apply {
Expand All @@ -42,6 +55,8 @@ class ApplicationSettingsPanel {
.addSeparator(5)
.addLabeledComponent("Inline completion trigger", completionTriggerModePanel, 5, false)
.addSeparator(5)
.addLabeledComponent("<html>Node binary<br/>(Requires restart IDE)</html>", nodeBinaryPanel, 5, false)
.addSeparator(5)
.addLabeledComponent("Anonymous usage tracking", isAnonymousUsageTrackingDisabledCheckBox, 5, false)
.addComponentFillVertically(JPanel(), 0)
.panel
Expand All @@ -65,6 +80,12 @@ class ApplicationSettingsPanel {
serverEndpointTextField.text = value
}

var nodeBinary: String
get() = nodeBinaryTextField.text
set(value) {
nodeBinaryTextField.text = value
}

var isAnonymousUsageTrackingDisabled: Boolean
get() = isAnonymousUsageTrackingDisabledCheckBox.isSelected
set(value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ class ApplicationSettingsState : PersistentStateComponent<ApplicationSettingsSta
field = value
stateFlow.value = this.data
}
var nodeBinary: String = ""
set(value) {
field = value
stateFlow.value = this.data
}
var isAnonymousUsageTrackingDisabled: Boolean = false
set(value) {
field = value
Expand All @@ -41,13 +46,15 @@ class ApplicationSettingsState : PersistentStateComponent<ApplicationSettingsSta
data class State(
val completionTriggerMode: TriggerMode,
val serverEndpoint: String,
val nodeBinary: String,
val isAnonymousUsageTrackingDisabled: Boolean,
)

val data: State
get() = State(
completionTriggerMode = completionTriggerMode,
serverEndpoint = serverEndpoint,
nodeBinary = nodeBinary,
isAnonymousUsageTrackingDisabled = isAnonymousUsageTrackingDisabled,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
actionManager.getAction("Tabby.CheckIssueDetail"),
actionManager.getAction("Tabby.ToggleInlineCompletionTriggerMode"),
actionManager.getAction("Tabby.OpenSettings"),
actionManager.getAction("Tabby.OpenOnlineDocs"),
)
}
},
Expand Down
Loading

0 comments on commit fceb8c9

Please sign in to comment.