feat(intellij): add option to specify node binary; improve docs. (#529)

r0.3
Zhiming Ma 2023-10-10 23:00:56 +08:00 committed by GitHub
parent 6dbb712918
commit fceb8c9217
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 142 additions and 172 deletions

View File

@ -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) {

View File

@ -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/")
}
}

View File

@ -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,

View File

@ -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

View File

@ -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
} }
} }

View File

@ -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) {

View File

@ -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,
) )

View File

@ -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"),
) )
} }
}, },

View File

@ -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"
}
}

View File

@ -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>