feat(intellij): add option to specify node binary; improve docs. (#529)
parent
6dbb712918
commit
fceb8c9217
|
|
@ -9,7 +9,9 @@ import com.intellij.openapi.components.service
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
import com.intellij.openapi.ui.Messages
|
import com.intellij.openapi.ui.Messages
|
||||||
import com.tabbyml.intellijtabby.agent.AgentService
|
import com.tabbyml.intellijtabby.agent.AgentService
|
||||||
|
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
class CheckIssueDetail : AnAction() {
|
class CheckIssueDetail : AnAction() {
|
||||||
private val logger = Logger.getInstance(CheckIssueDetail::class.java)
|
private val logger = Logger.getInstance(CheckIssueDetail::class.java)
|
||||||
|
|
@ -21,29 +23,35 @@ class CheckIssueDetail : AnAction() {
|
||||||
agentService.scope.launch {
|
agentService.scope.launch {
|
||||||
val detail = agentService.getCurrentIssueDetail() ?: return@launch
|
val detail = agentService.getCurrentIssueDetail() ?: return@launch
|
||||||
val serverHealthState = agentService.getServerHealthState()
|
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"]) {
|
val title = when (detail["name"]) {
|
||||||
"slowCompletionResponseTime" -> "Completion Requests Appear to Take Too Much Time"
|
"slowCompletionResponseTime" -> "Completion Requests Appear to Take Too Much Time"
|
||||||
"highCompletionTimeoutRate" -> "Most Completion Requests Timed Out"
|
"highCompletionTimeoutRate" -> "Most Completion Requests Timed Out"
|
||||||
else -> return@launch
|
else -> return@launch
|
||||||
}
|
}
|
||||||
val message = buildDetailMessage(detail, serverHealthState)
|
val message = buildDetailMessage(detail, serverHealthState, settingsState)
|
||||||
invokeLater {
|
invokeLater {
|
||||||
val result = Messages.showOkCancelDialog(message, title, "Dismiss", "Supported Models", Messages.getInformationIcon())
|
val result =
|
||||||
if (result == Messages.CANCEL) {
|
Messages.showOkCancelDialog(message, title, "Supported Models", "Dismiss", Messages.getInformationIcon())
|
||||||
|
if (result == Messages.OK) {
|
||||||
BrowserUtil.browse("https://tabby.tabbyml.com/docs/models/")
|
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 stats = detail["completionResponseStats"] as Map<*, *>?
|
||||||
val statsMessages = when (detail["name"]) {
|
val statsMessages = when (detail["name"]) {
|
||||||
"slowCompletionResponseTime" -> if (stats != null && stats["responses"] is Number && stats["averageResponseTime"] is Number) {
|
"slowCompletionResponseTime" -> if (stats != null && stats["responses"] is Number && stats["averageResponseTime"] is Number) {
|
||||||
val response = (stats["responses"] as Number).toInt()
|
val response = (stats["responses"] as Number).toInt()
|
||||||
val averageResponseTime = (stats["averageResponseTime"] 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 {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +59,7 @@ class CheckIssueDetail : AnAction() {
|
||||||
"highCompletionTimeoutRate" -> if (stats != null && stats["total"] is Number && stats["timeouts"] is Number) {
|
"highCompletionTimeoutRate" -> if (stats != null && stats["total"] is Number && stats["timeouts"] is Number) {
|
||||||
val timeout = (stats["timeouts"] as Number).toInt()
|
val timeout = (stats["timeouts"] as Number).toInt()
|
||||||
val total = (stats["total"] 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 {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
@ -63,27 +71,35 @@ class CheckIssueDetail : AnAction() {
|
||||||
val model = serverHealthState?.get("model") as String? ?: ""
|
val model = serverHealthState?.get("model") as String? ?: ""
|
||||||
val helpMessageForRunningLargeModelOnCPU = if (device == "cpu" && model.endsWith("B")) {
|
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.
|
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.
|
You can find supported model list in online documents.
|
||||||
"""
|
""".trimIndent()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
var helpMessage = ""
|
var commonHelpMessage = ""
|
||||||
if (helpMessageForRunningLargeModelOnCPU.isNotEmpty()) {
|
val host = URL(settingsState.serverEndpoint).host
|
||||||
helpMessage += helpMessageForRunningLargeModelOnCPU + "\n\n"
|
|
||||||
helpMessage += "Other possible causes of this issue are: \n"
|
|
||||||
} 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()) {
|
if (helpMessageForRunningLargeModelOnCPU.isEmpty()) {
|
||||||
helpMessage += " - The running model $model is too large to run on your Tabby server. ";
|
commonHelpMessage += "<li>The running model <i>$model</i> is too large to run on your Tabby server.<br/>"
|
||||||
helpMessage += "Please try a smaller model. You can find supported model list in online documents.\n";
|
commonHelpMessage += "Please try a smaller model. You can find supported model list in online documents.</li>"
|
||||||
}
|
}
|
||||||
return statsMessages + helpMessage
|
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<br/>"
|
||||||
|
if (commonHelpMessage.isNotEmpty()) {
|
||||||
|
helpMessage += "<br/>Other possible causes of this issue: <br/><ul>$commonHelpMessage</ul>"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// commonHelpMessage should not be empty here
|
||||||
|
helpMessage = "Possible causes of this issue: <br/><ul>$commonHelpMessage</ul>"
|
||||||
|
}
|
||||||
|
return "<html>$statsMessages<br/><br/>$helpMessage</html>"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun update(e: AnActionEvent) {
|
override fun update(e: AnActionEvent) {
|
||||||
|
|
|
||||||
|
|
@ -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/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,17 +10,20 @@ import com.intellij.execution.process.ProcessAdapter
|
||||||
import com.intellij.execution.process.ProcessEvent
|
import com.intellij.execution.process.ProcessEvent
|
||||||
import com.intellij.execution.process.ProcessOutputTypes
|
import com.intellij.execution.process.ProcessOutputTypes
|
||||||
import com.intellij.ide.plugins.PluginManagerCore
|
import com.intellij.ide.plugins.PluginManagerCore
|
||||||
|
import com.intellij.openapi.components.service
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
import com.intellij.openapi.extensions.PluginId
|
import com.intellij.openapi.extensions.PluginId
|
||||||
import com.intellij.openapi.util.Key
|
import com.intellij.openapi.util.Key
|
||||||
import com.intellij.util.EnvironmentUtil
|
import com.intellij.util.EnvironmentUtil
|
||||||
import com.intellij.util.io.BaseOutputReader
|
import com.intellij.util.io.BaseOutputReader
|
||||||
|
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
import java.io.File
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.io.OutputStreamWriter
|
import java.io.OutputStreamWriter
|
||||||
|
|
||||||
|
|
@ -46,27 +49,21 @@ class Agent : ProcessAdapter() {
|
||||||
|
|
||||||
open class AgentException(message: String) : Exception(message)
|
open class AgentException(message: String) : Exception(message)
|
||||||
|
|
||||||
|
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."
|
||||||
|
)
|
||||||
|
|
||||||
|
open class NodeBinaryNotFoundException : NodeBinaryException(
|
||||||
|
message = "Cannot find Node binary."
|
||||||
|
)
|
||||||
|
|
||||||
|
open class NodeBinaryInvalidVersionException(version: String) : NodeBinaryException(
|
||||||
|
message = "Node version is too old: $version."
|
||||||
|
)
|
||||||
|
|
||||||
fun open() {
|
fun open() {
|
||||||
logger.info("Environment variables: PATH: ${EnvironmentUtil.getValue("PATH")}")
|
val node = getNodeBinary()
|
||||||
|
val script = getNodeScript()
|
||||||
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.")
|
|
||||||
}
|
|
||||||
|
|
||||||
checkNodeVersion(node.absolutePath)
|
|
||||||
|
|
||||||
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.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val cmd = GeneralCommandLine(node.absolutePath, script.absolutePath)
|
val cmd = GeneralCommandLine(node.absolutePath, script.absolutePath)
|
||||||
process = object : KillableProcessHandler(cmd) {
|
process = object : KillableProcessHandler(cmd) {
|
||||||
override fun readerOptions(): BaseOutputReader.Options {
|
override fun readerOptions(): BaseOutputReader.Options {
|
||||||
|
|
@ -78,25 +75,56 @@ class Agent : ProcessAdapter() {
|
||||||
streamWriter = process.processInput.writer()
|
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 {
|
try {
|
||||||
val process = GeneralCommandLine(node, "--version").createProcess()
|
val process = GeneralCommandLine(node.absolutePath, "--version").createProcess()
|
||||||
val version = BufferedReader(InputStreamReader(process.inputStream)).readLine()
|
val version = BufferedReader(InputStreamReader(process.inputStream)).readLine()
|
||||||
val regResult = Regex("v([0-9]+)\\.([0-9]+)\\.([0-9]+)").find(version)
|
val regResult = Regex("v([0-9]+)\\.([0-9]+)\\.([0-9]+)").find(version)
|
||||||
if (regResult != null && regResult.groupValues[1].toInt() >= 18) {
|
if (regResult != null && regResult.groupValues[1].toInt() >= 18) {
|
||||||
return
|
return
|
||||||
} else {
|
} 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) {
|
} catch (e: Exception) {
|
||||||
if (e is AgentException) {
|
if (e is AgentException) {
|
||||||
throw e
|
throw e
|
||||||
} else {
|
} 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(
|
data class Config(
|
||||||
val server: Server? = null,
|
val server: Server? = null,
|
||||||
val completion: Completion? = null,
|
val completion: Completion? = null,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import com.intellij.openapi.progress.ProgressIndicator
|
||||||
import com.intellij.psi.PsiDocumentManager
|
import com.intellij.psi.PsiDocumentManager
|
||||||
import com.intellij.psi.PsiFile
|
import com.intellij.psi.PsiFile
|
||||||
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
|
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
|
||||||
import com.tabbyml.intellijtabby.usage.AnonymousUsageLogger
|
|
||||||
import io.ktor.util.*
|
import io.ktor.util.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -62,9 +61,7 @@ class AgentService : Disposable {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val settings = service<ApplicationSettingsState>()
|
val settings = service<ApplicationSettingsState>()
|
||||||
val anonymousUsageLogger = service<AnonymousUsageLogger>()
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|
||||||
val config = createAgentConfig(settings.data)
|
val config = createAgentConfig(settings.data)
|
||||||
val clientProperties = createClientProperties(settings.data)
|
val clientProperties = createClientProperties(settings.data)
|
||||||
try {
|
try {
|
||||||
|
|
@ -75,18 +72,14 @@ class AgentService : Disposable {
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
initResultFlow.value = false
|
initResultFlow.value = false
|
||||||
logger.warn("Agent init failed: $e")
|
logger.warn("Agent init failed: $e")
|
||||||
anonymousUsageLogger.event(
|
|
||||||
"IntelliJInitFailed", mapOf(
|
|
||||||
"client" to clientProperties.session["client"] as String, "error" to e.stackTraceToString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val notification = Notification(
|
val notification = Notification(
|
||||||
"com.tabbyml.intellijtabby.notification.warning",
|
"com.tabbyml.intellijtabby.notification.warning",
|
||||||
"Tabby initialization failed",
|
"Tabby initialization failed",
|
||||||
"${e.message}",
|
"${e.message}",
|
||||||
NotificationType.ERROR,
|
NotificationType.ERROR,
|
||||||
)
|
)
|
||||||
// FIXME: Add action to open FAQ page to help user set up nodejs.
|
notification.addAction(ActionManager.getInstance().getAction("Tabby.OpenOnlineDocs"))
|
||||||
invokeLater {
|
invokeLater {
|
||||||
initFailedNotification?.expire()
|
initFailedNotification?.expire()
|
||||||
initFailedNotification = notification
|
initFailedNotification = notification
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class ApplicationConfigurable : Configurable {
|
||||||
val settings = service<ApplicationSettingsState>()
|
val settings = service<ApplicationSettingsState>()
|
||||||
return settingsPanel.completionTriggerMode != settings.completionTriggerMode
|
return settingsPanel.completionTriggerMode != settings.completionTriggerMode
|
||||||
|| settingsPanel.serverEndpoint != settings.serverEndpoint
|
|| settingsPanel.serverEndpoint != settings.serverEndpoint
|
||||||
|
|| settingsPanel.nodeBinary != settings.nodeBinary
|
||||||
|| settingsPanel.isAnonymousUsageTrackingDisabled != settings.isAnonymousUsageTrackingDisabled
|
|| settingsPanel.isAnonymousUsageTrackingDisabled != settings.isAnonymousUsageTrackingDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,6 +28,7 @@ class ApplicationConfigurable : Configurable {
|
||||||
val settings = service<ApplicationSettingsState>()
|
val settings = service<ApplicationSettingsState>()
|
||||||
settings.completionTriggerMode = settingsPanel.completionTriggerMode
|
settings.completionTriggerMode = settingsPanel.completionTriggerMode
|
||||||
settings.serverEndpoint = settingsPanel.serverEndpoint
|
settings.serverEndpoint = settingsPanel.serverEndpoint
|
||||||
|
settings.nodeBinary = settingsPanel.nodeBinary
|
||||||
settings.isAnonymousUsageTrackingDisabled = settingsPanel.isAnonymousUsageTrackingDisabled
|
settings.isAnonymousUsageTrackingDisabled = settingsPanel.isAnonymousUsageTrackingDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,6 +36,7 @@ class ApplicationConfigurable : Configurable {
|
||||||
val settings = service<ApplicationSettingsState>()
|
val settings = service<ApplicationSettingsState>()
|
||||||
settingsPanel.completionTriggerMode = settings.completionTriggerMode
|
settingsPanel.completionTriggerMode = settings.completionTriggerMode
|
||||||
settingsPanel.serverEndpoint = settings.serverEndpoint
|
settingsPanel.serverEndpoint = settings.serverEndpoint
|
||||||
|
settingsPanel.nodeBinary = settings.nodeBinary
|
||||||
settingsPanel.isAnonymousUsageTrackingDisabled = settings.isAnonymousUsageTrackingDisabled
|
settingsPanel.isAnonymousUsageTrackingDisabled = settings.isAnonymousUsageTrackingDisabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -15,13 +15,26 @@ class ApplicationSettingsPanel {
|
||||||
"""
|
"""
|
||||||
<html>
|
<html>
|
||||||
A http or https URL of Tabby server endpoint.<br/>
|
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>.
|
Default to <i>http://localhost:8080</i>.
|
||||||
</html>
|
</html>
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
.panel
|
.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 completionTriggerModeAutomaticRadioButton = JBRadioButton("Automatic")
|
||||||
private val completionTriggerModeManualRadioButton = JBRadioButton("Manual")
|
private val completionTriggerModeManualRadioButton = JBRadioButton("Manual")
|
||||||
private val completionTriggerModeRadioGroup = ButtonGroup().apply {
|
private val completionTriggerModeRadioGroup = ButtonGroup().apply {
|
||||||
|
|
@ -42,6 +55,8 @@ class ApplicationSettingsPanel {
|
||||||
.addSeparator(5)
|
.addSeparator(5)
|
||||||
.addLabeledComponent("Inline completion trigger", completionTriggerModePanel, 5, false)
|
.addLabeledComponent("Inline completion trigger", completionTriggerModePanel, 5, false)
|
||||||
.addSeparator(5)
|
.addSeparator(5)
|
||||||
|
.addLabeledComponent("<html>Node binary<br/>(Requires restart IDE)</html>", nodeBinaryPanel, 5, false)
|
||||||
|
.addSeparator(5)
|
||||||
.addLabeledComponent("Anonymous usage tracking", isAnonymousUsageTrackingDisabledCheckBox, 5, false)
|
.addLabeledComponent("Anonymous usage tracking", isAnonymousUsageTrackingDisabledCheckBox, 5, false)
|
||||||
.addComponentFillVertically(JPanel(), 0)
|
.addComponentFillVertically(JPanel(), 0)
|
||||||
.panel
|
.panel
|
||||||
|
|
@ -65,6 +80,12 @@ class ApplicationSettingsPanel {
|
||||||
serverEndpointTextField.text = value
|
serverEndpointTextField.text = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var nodeBinary: String
|
||||||
|
get() = nodeBinaryTextField.text
|
||||||
|
set(value) {
|
||||||
|
nodeBinaryTextField.text = value
|
||||||
|
}
|
||||||
|
|
||||||
var isAnonymousUsageTrackingDisabled: Boolean
|
var isAnonymousUsageTrackingDisabled: Boolean
|
||||||
get() = isAnonymousUsageTrackingDisabledCheckBox.isSelected
|
get() = isAnonymousUsageTrackingDisabledCheckBox.isSelected
|
||||||
set(value) {
|
set(value) {
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ class ApplicationSettingsState : PersistentStateComponent<ApplicationSettingsSta
|
||||||
field = value
|
field = value
|
||||||
stateFlow.value = this.data
|
stateFlow.value = this.data
|
||||||
}
|
}
|
||||||
|
var nodeBinary: String = ""
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
stateFlow.value = this.data
|
||||||
|
}
|
||||||
var isAnonymousUsageTrackingDisabled: Boolean = false
|
var isAnonymousUsageTrackingDisabled: Boolean = false
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
|
|
@ -41,6 +46,7 @@ class ApplicationSettingsState : PersistentStateComponent<ApplicationSettingsSta
|
||||||
data class State(
|
data class State(
|
||||||
val completionTriggerMode: TriggerMode,
|
val completionTriggerMode: TriggerMode,
|
||||||
val serverEndpoint: String,
|
val serverEndpoint: String,
|
||||||
|
val nodeBinary: String,
|
||||||
val isAnonymousUsageTrackingDisabled: Boolean,
|
val isAnonymousUsageTrackingDisabled: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -48,6 +54,7 @@ class ApplicationSettingsState : PersistentStateComponent<ApplicationSettingsSta
|
||||||
get() = State(
|
get() = State(
|
||||||
completionTriggerMode = completionTriggerMode,
|
completionTriggerMode = completionTriggerMode,
|
||||||
serverEndpoint = serverEndpoint,
|
serverEndpoint = serverEndpoint,
|
||||||
|
nodeBinary = nodeBinary,
|
||||||
isAnonymousUsageTrackingDisabled = isAnonymousUsageTrackingDisabled,
|
isAnonymousUsageTrackingDisabled = isAnonymousUsageTrackingDisabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
|
||||||
actionManager.getAction("Tabby.CheckIssueDetail"),
|
actionManager.getAction("Tabby.CheckIssueDetail"),
|
||||||
actionManager.getAction("Tabby.ToggleInlineCompletionTriggerMode"),
|
actionManager.getAction("Tabby.ToggleInlineCompletionTriggerMode"),
|
||||||
actionManager.getAction("Tabby.OpenSettings"),
|
actionManager.getAction("Tabby.OpenSettings"),
|
||||||
|
actionManager.getAction("Tabby.OpenOnlineDocs"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
package com.tabbyml.intellijtabby.usage
|
|
||||||
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.intellij.openapi.components.Service
|
|
||||||
import com.intellij.openapi.components.service
|
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
|
||||||
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.OutputStreamWriter
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.URL
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.io.path.Path
|
|
||||||
|
|
||||||
@Service
|
|
||||||
class AnonymousUsageLogger {
|
|
||||||
private val logger = Logger.getInstance(AnonymousUsageLogger::class.java)
|
|
||||||
private val gson = Gson()
|
|
||||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
|
|
||||||
private lateinit var anonymousId: String
|
|
||||||
private val disabled: Boolean
|
|
||||||
get() {
|
|
||||||
return service<ApplicationSettingsState>().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<String, String>,
|
|
||||||
)
|
|
||||||
|
|
||||||
suspend fun event(event: String, properties: Map<String, String>) {
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
<h2 id="demo">Demo</h2>
|
<h2 id="demo">Demo</h2>
|
||||||
<p>Try our online demo <a href="https://tabby.tabbyml.com/playground/">here</a>.</p>
|
<p>Try our online demo <a href="https://tabby.tabbyml.com/playground/">here</a>.</p>
|
||||||
<h2 id="requirements">Requirements</h2>
|
<h2 id="requirements">Requirements</h2>
|
||||||
Tabby plugin requires <a href="https://nodejs.org/">Node.js</a> v18+ installed and added into <code>PATH</code> environment variable. </p>
|
Tabby plugin requires <a href="https://nodejs.org/">Node.js</a> v18+ installed. </p>
|
||||||
]]></description>
|
]]></description>
|
||||||
|
|
||||||
<!-- Product and plugin compatibility requirements.
|
<!-- Product and plugin compatibility requirements.
|
||||||
|
|
@ -86,11 +86,17 @@
|
||||||
<action id="Tabby.ToggleInlineCompletionTriggerMode"
|
<action id="Tabby.ToggleInlineCompletionTriggerMode"
|
||||||
class="com.tabbyml.intellijtabby.actions.ToggleInlineCompletionTriggerMode">
|
class="com.tabbyml.intellijtabby.actions.ToggleInlineCompletionTriggerMode">
|
||||||
</action>
|
</action>
|
||||||
|
<separator/>
|
||||||
<action id="Tabby.OpenSettings"
|
<action id="Tabby.OpenSettings"
|
||||||
class="com.tabbyml.intellijtabby.actions.OpenSettings"
|
class="com.tabbyml.intellijtabby.actions.OpenSettings"
|
||||||
text="Open Settings..."
|
text="Open Settings..."
|
||||||
description="Show settings for Tabby.">
|
description="Show settings for Tabby.">
|
||||||
</action>
|
</action>
|
||||||
|
<action id="Tabby.OpenOnlineDocs"
|
||||||
|
class="com.tabbyml.intellijtabby.actions.OpenOnlineDocs"
|
||||||
|
text="Open Online Help..."
|
||||||
|
description="Open the online docs in your web browser.">
|
||||||
|
</action>
|
||||||
</group>
|
</group>
|
||||||
</actions>
|
</actions>
|
||||||
</idea-plugin>
|
</idea-plugin>
|
||||||
Loading…
Reference in New Issue