feat(intellij): add check connection button in settings page. (#809)

* feat(intellij): add check connection in settings page.

* fix(intellij): update notification message.

* fix(intellij): fix language mapping for file extension.
release-fix-intellij-update-support-version-range
Zhiming Ma 2023-11-17 13:34:41 +08:00 committed by GitHub
parent d47dac9041
commit 76679bc249
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 323 additions and 167 deletions

1
clients/intellij/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.wasm filter=lfs diff=lfs merge=lfs -text

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1b69c5af834fd23053238e484c7fe9ed2f121d5b1fe32242af78576d67e49f1e
size 240169

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:72d0f97ba6c3134d7873ec5c9d0fd3c1f5137f4eac4dda0709993d92809e62b6
size 474189

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1190cddd839b78c2aec737573399a71c23fe9a546d3543f86304c4c68ca73852
size 990787

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:273f9ce6f2c595ad4e63b3195513b61974ae1ec513efcce39da1afa90574ef38
size 844087

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:060422a330f9c819a10e7310788d336dbcb53cc6a4be0e91d40f644564080f97
size 1182114

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:17382e1a69bd628107e8dfe37d31d57f7ba948e5f2da77e56a8aa010488dc5ae
size 186526

View File

@ -5,7 +5,7 @@
"repository": "https://github.com/TabbyML/tabby",
"scripts": {
"preupgrade-agent": "cd ../tabby-agent && yarn build",
"upgrade-agent": "rimraf ./node_scripts && cpy ../tabby-agent/dist/cli.js ./node_scripts/ --flat --rename=tabby-agent.js"
"upgrade-agent": "rimraf ./node_scripts && cpy ../tabby-agent/dist/cli.js ./node_scripts/ --flat --rename=tabby-agent.js && cpy ../tabby-agent/dist/wasm/* ./node_scripts/wasm/ --flat"
},
"devDependencies": {
"cpy-cli": "^4.2.0",

View File

@ -1,6 +1,5 @@
package com.tabbyml.intellijtabby.actions
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
@ -10,7 +9,6 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.ui.Messages
import com.tabbyml.intellijtabby.agent.Agent
import com.tabbyml.intellijtabby.agent.AgentService
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import kotlinx.coroutines.launch
import java.net.URL
@ -23,17 +21,25 @@ class CheckIssueDetail : AnAction() {
agentService.scope.launch {
val detail = agentService.getCurrentIssueDetail() ?: return@launch
val serverHealthState = agentService.getServerHealthState()
val agentConfig = agentService.getConfig()
logger.info("Show issue detail: $detail, $serverHealthState, $agentConfig")
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, agentConfig)
invokeLater {
Messages.showMessageDialog(message, title, Messages.getInformationIcon())
if (detail["name"] == "connectionFailed") {
invokeLater {
val messages = "<html>" + (detail["message"] as String?)?.replace("\n", "<br/>") + "</html>"
Messages.showErrorDialog(messages, "Cannot Connect to Tabby Server")
}
return@launch
} else {
val serverHealthState = agentService.getServerHealthState()
val agentConfig = agentService.getConfig()
logger.info("Show issue detail: $detail, $serverHealthState, $agentConfig")
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, agentConfig)
invokeLater {
Messages.showInfoMessage(message, title)
}
}
}
}

View File

@ -21,11 +21,8 @@ import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import io.ktor.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@Service
class AgentService : Disposable {
@ -89,14 +86,20 @@ class AgentService : Disposable {
}
scope.launch {
settings.state.collect {
if (it.serverEndpoint.isNotBlank()) {
updateConfig("server.endpoint", it.serverEndpoint)
} else {
clearConfig("server.endpoint")
}
updateClientProperties("user", "intellij.triggerMode", it.completionTriggerMode)
updateConfig("anonymousUsageTracking.disable", it.isAnonymousUsageTrackingDisabled)
settings.serverEndpointState.collect {
setEndpoint(it)
}
}
scope.launch {
settings.completionTriggerModeState.collect {
updateClientProperties("user", "intellij.triggerMode", it)
}
}
scope.launch {
settings.isAnonymousUsageTrackingDisabledState.collect {
updateConfig("anonymousUsageTracking.disable", it)
}
}
@ -117,20 +120,43 @@ class AgentService : Disposable {
}
}
scope.launch {
agent.status.collect { status ->
if (status == Agent.Status.READY) {
completionResponseWarningShown = false
}
}
}
scope.launch {
agent.currentIssue.collect { issueName ->
val content = when (issueName) {
"slowCompletionResponseTime" -> "Completion requests appear to take too much time"
"highCompletionTimeoutRate" -> "Most completion requests timed out"
else -> return@collect
val message = when (issueName) {
"connectionFailed" -> "Cannot connect to Tabby server"
"slowCompletionResponseTime" -> if (!completionResponseWarningShown) {
completionResponseWarningShown = true
"Completion requests appear to take too much time"
} else {
return@collect
}
"highCompletionTimeoutRate" -> if (!completionResponseWarningShown) {
completionResponseWarningShown = true
"Most completion requests timed out"
} else {
return@collect
}
else -> {
invokeLater {
issueNotification?.expire()
}
return@collect
}
}
if (completionResponseWarningShown) {
return@collect
}
completionResponseWarningShown = true
val notification = Notification(
"com.tabbyml.intellijtabby.notification.warning",
content,
message,
NotificationType.WARNING,
)
notification.addAction(ActionManager.getInstance().getAction("Tabby.CheckIssueDetail"))
@ -202,6 +228,14 @@ class AgentService : Disposable {
agent.clearConfig(key)
}
suspend fun setEndpoint(endpoint: String) {
if (endpoint.isNotBlank()) {
updateConfig("server.endpoint", endpoint)
} else {
clearConfig("server.endpoint")
}
}
suspend fun getConfig(): Agent.Config {
waitForInitialized()
return agent.getConfig()
@ -287,22 +321,24 @@ class AgentService : Disposable {
companion object {
// Language id: https://code.visualstudio.com/docs/languages/identifiers
private fun PsiFile.getLanguageId(): String {
if (this.language != Language.ANY && this.language.id.toLowerCasePreservingASCIIRules() !in arrayOf(
"txt",
"text",
"textmate"
)
return if (this.language != Language.ANY &&
this.language.id.isNotBlank() &&
this.language.id.toLowerCasePreservingASCIIRules() !in arrayOf("txt", "text", "textmate")
) {
if (languageIdMap.containsKey(this.language.id)) {
return languageIdMap[this.language.id]!!
}
return this.language.id.toLowerCasePreservingASCIIRules().replace("#", "sharp").replace("++", "pp")
languageIdMap[this.language.id] ?: this.language.id
.toLowerCasePreservingASCIIRules()
.replace("#", "sharp")
.replace("++", "pp")
.replace(" ", "")
}
return if (filetypeMap.containsKey(this.fileType.defaultExtension)) {
filetypeMap[this.fileType.defaultExtension]!!
} else {
this.fileType.defaultExtension.toLowerCasePreservingASCIIRules()
val ext = this.fileType.defaultExtension.ifBlank {
this.virtualFile.name.substringAfterLast(".")
}
if (ext.isNotBlank()) {
filetypeMap[ext] ?: ext.toLowerCasePreservingASCIIRules()
} else {
"plaintext"
}
}
}

View File

@ -25,10 +25,12 @@ class CompletionProvider {
clear()
val job = agentService.scope.launch {
logger.info("Trigger completion at $offset")
agentService.provideCompletion(editor, offset, manually)?.let {
logger.info("Show completion at $offset: $it")
inlineCompletionService.show(editor, offset, it)
agentService.provideCompletion(editor, offset, manually).let {
ongoingCompletionFlow.value = null
if (it != null) {
logger.info("Show completion at $offset: $it")
inlineCompletionService.show(editor, offset, it)
}
}
}
ongoingCompletionFlow.value = CompletionContext(editor, offset, job)

View File

@ -1,5 +1,12 @@
package com.tabbyml.intellijtabby.settings
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.components.service
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.ui.Messages
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBRadioButton
@ -7,7 +14,12 @@ import com.intellij.ui.components.JBTextField
import com.intellij.util.ui.FormBuilder
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.tabbyml.intellijtabby.agent.Agent
import com.tabbyml.intellijtabby.agent.AgentService
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import javax.swing.ButtonGroup
import javax.swing.JButton
import javax.swing.JPanel
private fun FormBuilder.addCopyableTooltip(text: String): FormBuilder {
@ -26,6 +38,82 @@ private fun FormBuilder.addCopyableTooltip(text: String): FormBuilder {
class ApplicationSettingsPanel {
private val serverEndpointTextField = JBTextField()
private val serverEndpointCheckConnectionButton = JButton("Check connection").apply {
addActionListener {
val parentComponent = this@ApplicationSettingsPanel.mainPanel
val agentService = service<AgentService>()
val settings = service<ApplicationSettingsState>()
val task = object : Task.Modal(
null,
parentComponent,
"Check Connection",
true
) {
lateinit var job: Job
override fun run(indicator: ProgressIndicator) {
job = agentService.scope.launch {
indicator.isIndeterminate = true
indicator.text = "Checking connection..."
settings.serverEndpoint = serverEndpointTextField.text
agentService.setEndpoint(serverEndpointTextField.text)
when (agentService.status.value) {
Agent.Status.READY -> {
invokeLater(ModalityState.stateForComponent(parentComponent)) {
Messages.showInfoMessage(
parentComponent,
"Successfully connected to the Tabby server.",
"Check Connection Completed"
)
}
}
Agent.Status.UNAUTHORIZED -> {
agentService.requestAuth(indicator)
if (agentService.status.value == Agent.Status.READY) {
invokeLater(ModalityState.stateForComponent(parentComponent)) {
Messages.showInfoMessage(
parentComponent,
"Successfully connected to the Tabby server.",
"Check Connection Completed"
)
}
} else {
invokeLater(ModalityState.stateForComponent(parentComponent)) {
Messages.showErrorDialog(
parentComponent,
"Failed to connect to the Tabby server.",
"Check Connection Failed"
)
}
}
}
else -> {
val detail = agentService.getCurrentIssueDetail()
if (detail?.get("name") == "connectionFailed") {
invokeLater(ModalityState.stateForComponent(parentComponent)) {
val errorMessage = (detail["message"] as String?)?.replace("\n", "<br/>") ?: ""
val messages = "<html>Failed to connect to the Tabby server:<br/>${errorMessage}</html>"
Messages.showErrorDialog(parentComponent, messages, "Check Connection Failed")
}
}
}
}
}
while (job.isActive) {
indicator.checkCanceled()
Thread.sleep(100)
}
}
override fun onCancel() {
job.cancel()
}
}
ProgressManager.getInstance().run(task)
}
}
private val serverEndpointPanel = FormBuilder.createFormBuilder()
.addComponent(serverEndpointTextField)
.addCopyableTooltip(
@ -37,6 +125,7 @@ class ApplicationSettingsPanel {
</html>
""".trimIndent()
)
.addComponent(serverEndpointCheckConnectionButton)
.panel
private val nodeBinaryTextField = JBTextField()

View File

@ -6,8 +6,9 @@ import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.util.xmlb.XmlSerializerUtil
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
@Service
@State(
@ -18,29 +19,41 @@ class ApplicationSettingsState : PersistentStateComponent<ApplicationSettingsSta
enum class TriggerMode {
@SerializedName("manual")
MANUAL,
@SerializedName("automatic")
AUTOMATIC,
}
private val completionTriggerModeFlow = MutableStateFlow(TriggerMode.AUTOMATIC)
val completionTriggerModeState = completionTriggerModeFlow.asStateFlow()
var completionTriggerMode: TriggerMode = TriggerMode.AUTOMATIC
set(value) {
field = value
stateFlow.value = this.data
completionTriggerModeFlow.value = value
}
private val serverEndpointFlow = MutableStateFlow("")
val serverEndpointState = serverEndpointFlow.asStateFlow()
var serverEndpoint: String = ""
set(value) {
field = value
stateFlow.value = this.data
serverEndpointFlow.value = value
}
private val nodeBinaryFlow = MutableStateFlow("")
val nodeBinaryState = nodeBinaryFlow.asStateFlow()
var nodeBinary: String = ""
set(value) {
field = value
stateFlow.value = this.data
nodeBinaryFlow.value = value
}
private val isAnonymousUsageTrackingDisabledFlow = MutableStateFlow(false)
val isAnonymousUsageTrackingDisabledState = isAnonymousUsageTrackingDisabledFlow.asStateFlow()
var isAnonymousUsageTrackingDisabled: Boolean = false
set(value) {
field = value
stateFlow.value = this.data
isAnonymousUsageTrackingDisabledFlow.value = value
}
data class State(
@ -58,8 +71,19 @@ class ApplicationSettingsState : PersistentStateComponent<ApplicationSettingsSta
isAnonymousUsageTrackingDisabled = isAnonymousUsageTrackingDisabled,
)
private val stateFlow = MutableStateFlow(data)
val state = stateFlow.asStateFlow()
val state = combine(
completionTriggerModeState,
serverEndpointState,
nodeBinaryState,
isAnonymousUsageTrackingDisabledState,
) { completionTriggerMode, serverEndpoint, nodeBinary, isAnonymousUsageTrackingDisabled ->
State(
completionTriggerMode = completionTriggerMode,
serverEndpoint = serverEndpoint,
nodeBinary = nodeBinary,
isAnonymousUsageTrackingDisabled = isAnonymousUsageTrackingDisabled,
)
}.stateIn(CoroutineScope(Dispatchers.IO), SharingStarted.Eagerly, this.data)
override fun getState(): ApplicationSettingsState {
return this