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", "repository": "https://github.com/TabbyML/tabby",
"scripts": { "scripts": {
"preupgrade-agent": "cd ../tabby-agent && yarn build", "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": { "devDependencies": {
"cpy-cli": "^4.2.0", "cpy-cli": "^4.2.0",

View File

@ -1,6 +1,5 @@
package com.tabbyml.intellijtabby.actions package com.tabbyml.intellijtabby.actions
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.AnActionEvent
@ -10,7 +9,6 @@ import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.ui.Messages import com.intellij.openapi.ui.Messages
import com.tabbyml.intellijtabby.agent.Agent import com.tabbyml.intellijtabby.agent.Agent
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 import java.net.URL
@ -23,6 +21,13 @@ class CheckIssueDetail : AnAction() {
agentService.scope.launch { agentService.scope.launch {
val detail = agentService.getCurrentIssueDetail() ?: return@launch val detail = agentService.getCurrentIssueDetail() ?: return@launch
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 serverHealthState = agentService.getServerHealthState()
val agentConfig = agentService.getConfig() val agentConfig = agentService.getConfig()
logger.info("Show issue detail: $detail, $serverHealthState, $agentConfig") logger.info("Show issue detail: $detail, $serverHealthState, $agentConfig")
@ -33,7 +38,8 @@ class CheckIssueDetail : AnAction() {
} }
val message = buildDetailMessage(detail, serverHealthState, agentConfig) val message = buildDetailMessage(detail, serverHealthState, agentConfig)
invokeLater { invokeLater {
Messages.showMessageDialog(message, title, Messages.getInformationIcon()) Messages.showInfoMessage(message, title)
}
} }
} }
} }

View File

@ -21,11 +21,8 @@ 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 io.ktor.util.* import io.ktor.util.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@Service @Service
class AgentService : Disposable { class AgentService : Disposable {
@ -89,14 +86,20 @@ class AgentService : Disposable {
} }
scope.launch { scope.launch {
settings.state.collect { settings.serverEndpointState.collect {
if (it.serverEndpoint.isNotBlank()) { setEndpoint(it)
updateConfig("server.endpoint", it.serverEndpoint)
} else {
clearConfig("server.endpoint")
} }
updateClientProperties("user", "intellij.triggerMode", it.completionTriggerMode) }
updateConfig("anonymousUsageTracking.disable", it.isAnonymousUsageTrackingDisabled)
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 { scope.launch {
agent.currentIssue.collect { issueName -> agent.currentIssue.collect { issueName ->
val content = when (issueName) { val message = when (issueName) {
"slowCompletionResponseTime" -> "Completion requests appear to take too much time" "connectionFailed" -> "Cannot connect to Tabby server"
"highCompletionTimeoutRate" -> "Most completion requests timed out" "slowCompletionResponseTime" -> if (!completionResponseWarningShown) {
else -> return@collect completionResponseWarningShown = true
} "Completion requests appear to take too much time"
if (completionResponseWarningShown) { } else {
return@collect return@collect
} }
"highCompletionTimeoutRate" -> if (!completionResponseWarningShown) {
completionResponseWarningShown = true completionResponseWarningShown = true
"Most completion requests timed out"
} else {
return@collect
}
else -> {
invokeLater {
issueNotification?.expire()
}
return@collect
}
}
val notification = Notification( val notification = Notification(
"com.tabbyml.intellijtabby.notification.warning", "com.tabbyml.intellijtabby.notification.warning",
content, message,
NotificationType.WARNING, NotificationType.WARNING,
) )
notification.addAction(ActionManager.getInstance().getAction("Tabby.CheckIssueDetail")) notification.addAction(ActionManager.getInstance().getAction("Tabby.CheckIssueDetail"))
@ -202,6 +228,14 @@ class AgentService : Disposable {
agent.clearConfig(key) agent.clearConfig(key)
} }
suspend fun setEndpoint(endpoint: String) {
if (endpoint.isNotBlank()) {
updateConfig("server.endpoint", endpoint)
} else {
clearConfig("server.endpoint")
}
}
suspend fun getConfig(): Agent.Config { suspend fun getConfig(): Agent.Config {
waitForInitialized() waitForInitialized()
return agent.getConfig() return agent.getConfig()
@ -287,22 +321,24 @@ class AgentService : Disposable {
companion object { companion object {
// Language id: https://code.visualstudio.com/docs/languages/identifiers // Language id: https://code.visualstudio.com/docs/languages/identifiers
private fun PsiFile.getLanguageId(): String { private fun PsiFile.getLanguageId(): String {
if (this.language != Language.ANY && this.language.id.toLowerCasePreservingASCIIRules() !in arrayOf( return if (this.language != Language.ANY &&
"txt", this.language.id.isNotBlank() &&
"text", this.language.id.toLowerCasePreservingASCIIRules() !in arrayOf("txt", "text", "textmate")
"textmate"
)
) { ) {
if (languageIdMap.containsKey(this.language.id)) { languageIdMap[this.language.id] ?: this.language.id
return languageIdMap[this.language.id]!! .toLowerCasePreservingASCIIRules()
} .replace("#", "sharp")
return this.language.id.toLowerCasePreservingASCIIRules().replace("#", "sharp").replace("++", "pp") .replace("++", "pp")
.replace(" ", "") .replace(" ", "")
}
return if (filetypeMap.containsKey(this.fileType.defaultExtension)) {
filetypeMap[this.fileType.defaultExtension]!!
} else { } 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() clear()
val job = agentService.scope.launch { val job = agentService.scope.launch {
logger.info("Trigger completion at $offset") logger.info("Trigger completion at $offset")
agentService.provideCompletion(editor, offset, manually)?.let { agentService.provideCompletion(editor, offset, manually).let {
ongoingCompletionFlow.value = null
if (it != null) {
logger.info("Show completion at $offset: $it") logger.info("Show completion at $offset: $it")
inlineCompletionService.show(editor, offset, it) inlineCompletionService.show(editor, offset, it)
ongoingCompletionFlow.value = null }
} }
} }
ongoingCompletionFlow.value = CompletionContext(editor, offset, job) ongoingCompletionFlow.value = CompletionContext(editor, offset, job)

View File

@ -1,5 +1,12 @@
package com.tabbyml.intellijtabby.settings 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.JBCheckBox
import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBRadioButton 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.FormBuilder
import com.intellij.util.ui.JBUI import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil 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.ButtonGroup
import javax.swing.JButton
import javax.swing.JPanel import javax.swing.JPanel
private fun FormBuilder.addCopyableTooltip(text: String): FormBuilder { private fun FormBuilder.addCopyableTooltip(text: String): FormBuilder {
@ -26,6 +38,82 @@ private fun FormBuilder.addCopyableTooltip(text: String): FormBuilder {
class ApplicationSettingsPanel { class ApplicationSettingsPanel {
private val serverEndpointTextField = JBTextField() 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() private val serverEndpointPanel = FormBuilder.createFormBuilder()
.addComponent(serverEndpointTextField) .addComponent(serverEndpointTextField)
.addCopyableTooltip( .addCopyableTooltip(
@ -37,6 +125,7 @@ class ApplicationSettingsPanel {
</html> </html>
""".trimIndent() """.trimIndent()
) )
.addComponent(serverEndpointCheckConnectionButton)
.panel .panel
private val nodeBinaryTextField = JBTextField() 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.State
import com.intellij.openapi.components.Storage import com.intellij.openapi.components.Storage
import com.intellij.util.xmlb.XmlSerializerUtil import com.intellij.util.xmlb.XmlSerializerUtil
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
@Service @Service
@State( @State(
@ -18,29 +19,41 @@ class ApplicationSettingsState : PersistentStateComponent<ApplicationSettingsSta
enum class TriggerMode { enum class TriggerMode {
@SerializedName("manual") @SerializedName("manual")
MANUAL, MANUAL,
@SerializedName("automatic") @SerializedName("automatic")
AUTOMATIC, AUTOMATIC,
} }
private val completionTriggerModeFlow = MutableStateFlow(TriggerMode.AUTOMATIC)
val completionTriggerModeState = completionTriggerModeFlow.asStateFlow()
var completionTriggerMode: TriggerMode = TriggerMode.AUTOMATIC var completionTriggerMode: TriggerMode = TriggerMode.AUTOMATIC
set(value) { set(value) {
field = value field = value
stateFlow.value = this.data completionTriggerModeFlow.value = value
} }
private val serverEndpointFlow = MutableStateFlow("")
val serverEndpointState = serverEndpointFlow.asStateFlow()
var serverEndpoint: String = "" var serverEndpoint: String = ""
set(value) { set(value) {
field = value field = value
stateFlow.value = this.data serverEndpointFlow.value = value
} }
private val nodeBinaryFlow = MutableStateFlow("")
val nodeBinaryState = nodeBinaryFlow.asStateFlow()
var nodeBinary: String = "" var nodeBinary: String = ""
set(value) { set(value) {
field = value field = value
stateFlow.value = this.data nodeBinaryFlow.value = value
} }
private val isAnonymousUsageTrackingDisabledFlow = MutableStateFlow(false)
val isAnonymousUsageTrackingDisabledState = isAnonymousUsageTrackingDisabledFlow.asStateFlow()
var isAnonymousUsageTrackingDisabled: Boolean = false var isAnonymousUsageTrackingDisabled: Boolean = false
set(value) { set(value) {
field = value field = value
stateFlow.value = this.data isAnonymousUsageTrackingDisabledFlow.value = value
} }
data class State( data class State(
@ -58,8 +71,19 @@ class ApplicationSettingsState : PersistentStateComponent<ApplicationSettingsSta
isAnonymousUsageTrackingDisabled = isAnonymousUsageTrackingDisabled, isAnonymousUsageTrackingDisabled = isAnonymousUsageTrackingDisabled,
) )
private val stateFlow = MutableStateFlow(data) val state = combine(
val state = stateFlow.asStateFlow() 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 { override fun getState(): ApplicationSettingsState {
return this return this