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:
      $commonHelpMessage
    " + } } 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:
      $commonHelpMessage
    " } - 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.

    ]]>