diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/CheckIssueDetail.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/CheckIssueDetail.kt
index d0d0f664f90f..d5b33e0247c8 100644
--- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/CheckIssueDetail.kt
+++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/CheckIssueDetail.kt
@@ -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)
@@ -21,29 +23,35 @@ 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().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, serverHealthState: Map?): String {
+ private fun buildDetailMessage(
+ detail: Map,
+ serverHealthState: Map?,
+ 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 {
""
}
@@ -51,7 +59,7 @@ class CheckIssueDetail : AnAction() {
"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 {
""
}
@@ -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 $model 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 += "The running model $model is too large to run on your Tabby server.
"
+ commonHelpMessage += "Please try a smaller model. You can find supported model list in online documents."
+ }
+ if (!(host == "localhost" || host == "127.0.0.1")) {
+ commonHelpMessage += "A poor network connection. Please check your network and proxy settings."
+ commonHelpMessage += "Server overload. Please contact your Tabby server administrator for assistance."
+ }
+
+ var helpMessage: String
if (helpMessageForRunningLargeModelOnCPU.isNotEmpty()) {
- helpMessage += helpMessageForRunningLargeModelOnCPU + "\n\n"
- helpMessage += "Other possible causes of this issue are: \n"
+ helpMessage = "$helpMessageForRunningLargeModelOnCPU
"
+ if (commonHelpMessage.isNotEmpty()) {
+ helpMessage += "
Other possible causes of this issue:
"
+ }
} 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:
"
}
- return statsMessages + helpMessage
+ return "$statsMessages
$helpMessage"
}
override fun update(e: AnActionEvent) {
diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenOnlineDocs.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenOnlineDocs.kt
new file mode 100644
index 000000000000..acfacb5f4ec1
--- /dev/null
+++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenOnlineDocs.kt
@@ -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/")
+ }
+}
\ No newline at end of file
diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/Agent.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/Agent.kt
index 2ef2937e13b3..51e9703dc3a6 100644
--- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/Agent.kt
+++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/Agent.kt
@@ -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
@@ -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 {
@@ -78,25 +75,56 @@ class Agent : ProcessAdapter() {
streamWriter = process.processInput.writer()
}
- private fun checkNodeVersion(node: String) {
+ private fun getNodeBinary(): File {
+ val settings = service()
+ 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,
diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/AgentService.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/AgentService.kt
index 17a533eb277a..e746b40371e8 100644
--- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/AgentService.kt
+++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/AgentService.kt
@@ -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
@@ -62,9 +61,7 @@ class AgentService : Disposable {
init {
val settings = service()
- val anonymousUsageLogger = service()
scope.launch {
-
val config = createAgentConfig(settings.data)
val clientProperties = createClientProperties(settings.data)
try {
@@ -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
diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationConfigurable.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationConfigurable.kt
index 2ea03af9111c..aa89dd2fe863 100644
--- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationConfigurable.kt
+++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationConfigurable.kt
@@ -20,6 +20,7 @@ class ApplicationConfigurable : Configurable {
val settings = service()
return settingsPanel.completionTriggerMode != settings.completionTriggerMode
|| settingsPanel.serverEndpoint != settings.serverEndpoint
+ || settingsPanel.nodeBinary != settings.nodeBinary
|| settingsPanel.isAnonymousUsageTrackingDisabled != settings.isAnonymousUsageTrackingDisabled
}
@@ -27,6 +28,7 @@ class ApplicationConfigurable : Configurable {
val settings = service()
settings.completionTriggerMode = settingsPanel.completionTriggerMode
settings.serverEndpoint = settingsPanel.serverEndpoint
+ settings.nodeBinary = settingsPanel.nodeBinary
settings.isAnonymousUsageTrackingDisabled = settingsPanel.isAnonymousUsageTrackingDisabled
}
@@ -34,6 +36,7 @@ class ApplicationConfigurable : Configurable {
val settings = service()
settingsPanel.completionTriggerMode = settings.completionTriggerMode
settingsPanel.serverEndpoint = settings.serverEndpoint
+ settingsPanel.nodeBinary = settings.nodeBinary
settingsPanel.isAnonymousUsageTrackingDisabled = settings.isAnonymousUsageTrackingDisabled
}
}
\ No newline at end of file
diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationSettingsPanel.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationSettingsPanel.kt
index 75f8463f5f34..2827c8e314c1 100644
--- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationSettingsPanel.kt
+++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationSettingsPanel.kt
@@ -15,13 +15,26 @@ class ApplicationSettingsPanel {
"""
A http or https URL of Tabby server endpoint.
- If leave empty, server endpoint config in ~/.tabby-client/agent/config.toml will be used
+ If leave empty, server endpoint config in ~/.tabby-client/agent/config.toml will be used.
Default to http://localhost:8080.
""".trimIndent()
)
.panel
+ private val nodeBinaryTextField = JBTextField()
+ private val nodeBinaryPanel = FormBuilder.createFormBuilder()
+ .addComponent(nodeBinaryTextField)
+ .addTooltip(
+ """
+
+ Path to the Node binary for running the Tabby agent. The Node version must be >= 18.0.
+ If left empty, Tabby will attempt to find the Node binary in the PATH environment variable.
+
+ """.trimIndent()
+ )
+ .panel
+
private val completionTriggerModeAutomaticRadioButton = JBRadioButton("Automatic")
private val completionTriggerModeManualRadioButton = JBRadioButton("Manual")
private val completionTriggerModeRadioGroup = ButtonGroup().apply {
@@ -42,6 +55,8 @@ class ApplicationSettingsPanel {
.addSeparator(5)
.addLabeledComponent("Inline completion trigger", completionTriggerModePanel, 5, false)
.addSeparator(5)
+ .addLabeledComponent("Node binary
(Requires restart IDE)", nodeBinaryPanel, 5, false)
+ .addSeparator(5)
.addLabeledComponent("Anonymous usage tracking", isAnonymousUsageTrackingDisabledCheckBox, 5, false)
.addComponentFillVertically(JPanel(), 0)
.panel
@@ -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) {
diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationSettingsState.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationSettingsState.kt
index 77bc75b2089e..efa4ffdbe660 100644
--- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationSettingsState.kt
+++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/ApplicationSettingsState.kt
@@ -32,6 +32,11 @@ class ApplicationSettingsState : PersistentStateComponent().isAnonymousUsageTrackingDisabled
- }
- private val initialized = MutableStateFlow(false)
-
- init {
- scope.launch {
- try {
- val home = System.getProperty("user.home")
- logger.info("User home: $home")
- val datafile = Path(home).resolve(".tabby-client/agent/data.json").toFile()
- var data: Map<*, *>? = null
- try {
- val dataJson = datafile.inputStream().bufferedReader().use { it.readText() }
- data = gson.fromJson(dataJson, Map::class.java)
- } catch (e: Exception) {
- logger.info("Failed to load anonymous ID: ${e.message}")
- }
- if (data?.get("anonymousId") != null) {
- anonymousId = data["anonymousId"].toString()
- logger.info("Saved anonymous ID: $anonymousId")
- } else {
- anonymousId = UUID.randomUUID().toString()
- val newData = data?.toMutableMap() ?: mutableMapOf()
- newData["anonymousId"] = anonymousId
- val newDataJson = gson.toJson(newData)
- datafile.parentFile.mkdirs()
- datafile.writeText(newDataJson)
- logger.info("Create new anonymous ID: $anonymousId")
- }
- } catch (e: Exception) {
- logger.warn("Failed when init anonymous ID: ${e.message}")
- anonymousId = UUID.randomUUID().toString()
- } finally {
- initialized.value = true
- }
- }
- }
-
- data class UsageRequest(
- val distinctId: String,
- val event: String,
- val properties: Map,
- )
-
- suspend fun event(event: String, properties: Map) {
- initialized.first { it }
-
- if (disabled) {
- return
- }
-
- val request = UsageRequest(
- distinctId = anonymousId,
- event = event,
- properties = properties,
- )
- val requestString = gson.toJson(request)
-
- withContext(scope.coroutineContext) {
- try {
- val connection = URL(ENDPOINT).openConnection() as HttpURLConnection
- connection.requestMethod = "POST"
- connection.setRequestProperty("Content-Type", "application/json")
- connection.setRequestProperty("Accept", "application/json")
- connection.doInput = true
- connection.doOutput = true
-
- val outputStreamWriter = OutputStreamWriter(connection.outputStream)
- outputStreamWriter.write(requestString)
- outputStreamWriter.flush()
-
- val responseCode = connection.responseCode
- if (responseCode == HttpURLConnection.HTTP_OK) {
- logger.info("Usage event sent successfully: $requestString")
- } else {
- logger.warn("Usage event failed to send: $responseCode")
- }
- connection.disconnect()
- } catch (e: Exception) {
- logger.warn("Usage event failed to send: ${e.message}")
- }
- }
-
- }
-
- companion object {
- const val ENDPOINT = "https://app.tabbyml.com/api/usage"
- }
-}
\ No newline at end of file
diff --git a/clients/intellij/src/main/resources/META-INF/plugin.xml b/clients/intellij/src/main/resources/META-INF/plugin.xml
index d1a662d047f9..63eaa02d4588 100644
--- a/clients/intellij/src/main/resources/META-INF/plugin.xml
+++ b/clients/intellij/src/main/resources/META-INF/plugin.xml
@@ -22,7 +22,7 @@
Demo
Try our online demo here.
Requirements
- Tabby plugin requires Node.js v18+ installed and added into PATH
environment variable.
+ Tabby plugin requires Node.js v18+ installed.
]]>