Skip to content

Commit

Permalink
refactor(intellij): update lsp method to use tabby/status and `tabb…
Browse files Browse the repository at this point in the history
…y/config` instead of `tabby/agent/*`.
  • Loading branch information
icycodes committed Nov 22, 2024
1 parent 171354e commit 7e476e5
Show file tree
Hide file tree
Showing 17 changed files with 348 additions and 420 deletions.
Original file line number Diff line number Diff line change
@@ -1,109 +1,35 @@
package com.tabbyml.intellijtabby.actions

import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.components.service
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.components.serviceOrNull
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.tabbyml.intellijtabby.events.CombinedState
import com.tabbyml.intellijtabby.lsp.ConnectionService
import com.tabbyml.intellijtabby.lsp.protocol.IssueDetailParams
import com.tabbyml.intellijtabby.lsp.protocol.IssueName
import com.tabbyml.intellijtabby.settings.SettingsService
import com.tabbyml.intellijtabby.lsp.protocol.StatusInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import org.eclipse.lsp4j.ExecuteCommandParams

class CheckIssueDetail : AnAction() {
private val scope = CoroutineScope(Dispatchers.IO)

override fun actionPerformed(e: AnActionEvent) {
val project = e.getRequiredData(CommonDataKeys.PROJECT)
val combinedState = project.serviceOrNull<CombinedState>() ?: return
val issueName = combinedState.state.agentIssue ?: return

val settings = service<SettingsService>()
val connectionService = project.serviceOrNull<ConnectionService>() ?: return
val scope = CoroutineScope(Dispatchers.IO)

scope.launch {
val server = connectionService.getServerAsync() ?: return@launch
val detail = server.agentFeature.getIssueDetail(
IssueDetailParams(
name = issueName, helpMessageFormat = IssueDetailParams.HelpMessageFormat.HTML
)
).await() ?: return@launch
if (detail.name == IssueName.CONNECTION_FAILED) {
invokeLater {
val selected = Messages.showDialog(
"<html>${detail.helpMessage}</html>",
"Cannot Connect to Tabby Server",
arrayOf("OK", "Online Help"),
0,
Messages.getErrorIcon(),
)
when (selected) {
0 -> {
// OK
}

1 -> {
// Online Help
showOnlineHelp(e)
}
}
}
} else {
val title = when (detail.name) {
IssueName.SLOW_COMPLETION_RESPONSE_TIME -> "Completion Requests Appear to Take Too Much Time"
IssueName.HIGH_COMPLETION_TIMEOUT_RATE -> "Most Completion Requests Timed Out"
else -> return@launch
}
invokeLater {
val selected = Messages.showDialog(
"<html>${detail.helpMessage}</html>",
title,
arrayOf("OK", "Online Help", "Don't Show Again"),
0,
Messages.getWarningIcon(),
)
when (selected) {
0 -> {
// OK
}

1 -> {
// Online Help
showOnlineHelp(e)
}
val command = combinedState.state.agentStatus?.command ?: return@launch

2 -> {
// Don't Show Again
settings.notificationsMuted += listOf("completionResponseTimeIssues")
settings.notifyChanges(project)
}
}
}
}
}
}

private fun showOnlineHelp(e: AnActionEvent) {
e.project?.let {
invokeLater {
val actionManager = ActionManager.getInstance()
val actionGroup = actionManager.getAction("Tabby.OpenOnlineHelp") as ActionGroup
val popup = JBPopupFactory.getInstance().createActionGroupPopup(
"Online Help",
actionGroup,
e.dataContext,
false,
null,
10,
)
popup.showCenteredInCurrentWindow(it)
}
server.workspaceFeature.executeCommand(ExecuteCommandParams(
command.command,
command.arguments,
))
}
}

Expand All @@ -112,12 +38,8 @@ class CheckIssueDetail : AnAction() {
val project = e.getData(CommonDataKeys.PROJECT) ?: return
val combinedState = project.serviceOrNull<CombinedState>() ?: return

val muted = mutableListOf<String>()
if (combinedState.state.settings.notificationsMuted.contains("completionResponseTimeIssues")) {
muted += listOf(IssueName.SLOW_COMPLETION_RESPONSE_TIME, IssueName.HIGH_COMPLETION_TIMEOUT_RATE)
}
e.presentation.isVisible = combinedState.state.agentIssue != null && combinedState.state.agentIssue !in muted
e.presentation.icon = if (combinedState.state.agentIssue == IssueName.CONNECTION_FAILED) {
e.presentation.isVisible = combinedState.state.agentStatus?.command != null
e.presentation.icon = if (combinedState.state.agentStatus?.status == StatusInfo.Status.DISCONNECTED) {
AllIcons.General.Error
} else {
AllIcons.General.Warning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.components.service
import com.intellij.openapi.components.serviceOrNull
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.colors.EditorColorsListener
Expand All @@ -22,11 +23,16 @@ import com.intellij.ui.jcef.JBCefBrowserBase
import com.intellij.ui.jcef.JBCefJSQuery
import com.tabbyml.intellijtabby.events.CombinedState
import com.tabbyml.intellijtabby.git.GitProvider
import com.tabbyml.intellijtabby.lsp.protocol.ServerInfo
import com.tabbyml.intellijtabby.lsp.protocol.Status
import com.tabbyml.intellijtabby.lsp.ConnectionService
import com.tabbyml.intellijtabby.lsp.protocol.Config
import com.tabbyml.intellijtabby.lsp.protocol.StatusInfo
import com.tabbyml.intellijtabby.lsp.protocol.StatusRequestParams
import io.github.z4kn4fein.semver.Version
import io.github.z4kn4fein.semver.constraints.Constraint
import io.github.z4kn4fein.semver.constraints.satisfiedBy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.cef.browser.CefBrowser
import org.cef.handler.CefLoadHandlerAdapter
import java.awt.Color
Expand All @@ -53,10 +59,13 @@ class ChatBrowser(private val project: Project) : JBCefBrowser(
private val gitProvider = project.service<GitProvider>()
private val messageBusConnection = project.messageBus.connect()

private val scope = CoroutineScope(Dispatchers.IO)
private suspend fun getServer() = project.serviceOrNull<ConnectionService>()?.getServerAsync()

private val reloadHandler = JBCefJSQuery.create(this as JBCefBrowserBase)
private val chatPanelRequestHandler = JBCefJSQuery.create(this as JBCefBrowserBase)

private var currentConfig: ServerInfo.ServerInfoConfig? = null
private var currentConfig: Config.ServerConfig? = null
private var isChatPanelLoaded = false
private val pendingScripts: MutableList<String> = mutableListOf()

Expand Down Expand Up @@ -249,42 +258,49 @@ class ChatBrowser(private val project: Project) : JBCefBrowser(

private fun reloadContent(force: Boolean = false) {
if (force) {
// FIXME(@icycodes): force reload requires await reconnection then get server health
reloadContentInternal(true)
scope.launch {
val server = getServer() ?: return@launch
server.statusFeature.getStatus(StatusRequestParams(recheckConnection = true)).thenAccept {
reloadContentInternal(it, true)
}
}
} else {
reloadContentInternal(false)
reloadContentInternal(combinedState.state.agentStatus)
}
}

private fun reloadContentInternal(force: Boolean = false) {
val status = combinedState.state.agentStatus
when (status) {
Status.NOT_INITIALIZED, Status.FINALIZED -> {
showContent("Initializing...")
}
private fun reloadContentInternal(statusInfo: StatusInfo?, force: Boolean = false) {
if (statusInfo == null) {
showContent("Initializing...")
} else {
when (statusInfo.status) {
StatusInfo.Status.CONNECTING -> {
showContent("Connecting to Tabby server...")
}

Status.DISCONNECTED -> {
showContent("Cannot connect to Tabby server, please check your settings.")
}
StatusInfo.Status.UNAUTHORIZED -> {
showContent("Authorization required, please set your token in settings.")
}

Status.UNAUTHORIZED -> {
showContent("Authorization required, please set your token in settings.")
}
StatusInfo.Status.DISCONNECTED -> {
showContent("Cannot connect to Tabby server, please check your settings.")
}

else -> {
val health = combinedState.state.agentServerInfo?.health
val error = checkServerHealth(health)
if (error != null) {
showContent(error)
} else {
val config = combinedState.state.agentServerInfo?.config
if (config == null) {
showContent("Initializing...")
} else if (force || currentConfig != config) {
showContent("Loading chat panel...")
isChatPanelLoaded = false
currentConfig = config
jsLoadChatPanel()
else -> {
val health = statusInfo.serverHealth
val error = checkServerHealth(health)
if (error != null) {
showContent(error)
} else {
val config = combinedState.state.agentConfig?.server
if (config == null) {
showContent("Initializing...")
} else if (force || currentConfig != config) {
showContent("Loading chat panel...")
isChatPanelLoaded = false
currentConfig = config
jsLoadChatPanel()
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.util.messages.Topic
import com.tabbyml.intellijtabby.completion.InlineCompletionService
import com.tabbyml.intellijtabby.lsp.ConnectionService
import com.tabbyml.intellijtabby.lsp.LanguageClient
import com.tabbyml.intellijtabby.lsp.protocol.IssueList
import com.tabbyml.intellijtabby.lsp.protocol.ServerInfo
import com.tabbyml.intellijtabby.lsp.protocol.Status
import com.tabbyml.intellijtabby.lsp.protocol.Config
import com.tabbyml.intellijtabby.lsp.protocol.StatusInfo
import com.tabbyml.intellijtabby.notifications.hideAuthRequiredNotification
import com.tabbyml.intellijtabby.notifications.notifyAuthRequired
import com.tabbyml.intellijtabby.safeSyncPublisher
import com.tabbyml.intellijtabby.settings.SettingsService
Expand All @@ -22,47 +21,31 @@ class CombinedState(private val project: Project) : Disposable {
data class State(
val settings: SettingsService.Settings,
val connectionState: ConnectionService.State,
val agentStatus: String,
val agentIssue: String?,
val agentServerInfo: ServerInfo?,
val isInlineCompletionLoading: Boolean,
val agentStatus: StatusInfo?,
val agentConfig: Config?,
) {
fun withSettings(settings: SettingsService.Settings): State {
return State(settings, connectionState, agentStatus, agentIssue, agentServerInfo, isInlineCompletionLoading)
return State(settings, connectionState, agentStatus, agentConfig)
}

fun withConnectionState(connectionState: ConnectionService.State): State {
return State(settings, connectionState, agentStatus, agentIssue, agentServerInfo, isInlineCompletionLoading)
return State(settings, connectionState, agentStatus, agentConfig)
}

fun withAgentStatus(agentStatus: String): State {
return State(settings, connectionState, agentStatus, agentIssue, agentServerInfo, isInlineCompletionLoading)
fun withStatus(status: StatusInfo): State {
return State(settings, connectionState, status, agentConfig)
}

fun withAgentIssue(currentIssue: String?): State {
return State(settings, connectionState, agentStatus, currentIssue, agentServerInfo, isInlineCompletionLoading)
}

fun withoutAgentIssue(): State {
return withAgentIssue(null)
}

fun withAgentServerInfo(serverInfo: ServerInfo?): State {
return State(settings, connectionState, agentStatus, agentIssue, serverInfo, isInlineCompletionLoading)
}

fun withInlineCompletionLoading(isInlineCompletionLoading: Boolean = true): State {
return State(settings, connectionState, agentStatus, agentIssue, agentServerInfo, isInlineCompletionLoading)
fun withConfig(config: Config): State {
return State(settings, connectionState, agentStatus, config)
}
}

var state = State(
service<SettingsService>().settings(),
ConnectionService.State.INITIALIZING,
Status.NOT_INITIALIZED,
null,
null,
false,
)
private set

Expand All @@ -79,32 +62,21 @@ class CombinedState(private val project: Project) : Disposable {
project.safeSyncPublisher(Listener.TOPIC)?.stateChanged(this@CombinedState.state)
}
})
messageBusConnection.subscribe(LanguageClient.AgentListener.TOPIC, object : LanguageClient.AgentListener {
override fun agentStatusChanged(status: String) {
state = state.withAgentStatus(status)
messageBusConnection.subscribe(LanguageClient.StatusListener.TOPIC, object : LanguageClient.StatusListener {
override fun statusChanged(status: StatusInfo) {
state = state.withStatus(status)
project.safeSyncPublisher(Listener.TOPIC)?.stateChanged(state)

if (status == Status.UNAUTHORIZED) {
if (status.status == StatusInfo.Status.UNAUTHORIZED) {
notifyAuthRequired()
} else {
hideAuthRequiredNotification()
}
}

override fun agentIssueUpdated(issueList: IssueList) {
state = issueList.issues.firstOrNull()?.let {
state.withAgentIssue(it)
} ?: state.withoutAgentIssue()
project.safeSyncPublisher(Listener.TOPIC)?.stateChanged(state)
}

override fun agentServerInfoUpdated(serverInfo: ServerInfo) {
state = state.withAgentServerInfo(serverInfo)
project.safeSyncPublisher(Listener.TOPIC)?.stateChanged(state)
}
})

messageBusConnection.subscribe(InlineCompletionService.Listener.TOPIC, object : InlineCompletionService.Listener {
override fun loadingStateChanged(loading: Boolean) {
state = state.withInlineCompletionLoading(loading)
messageBusConnection.subscribe(LanguageClient.ConfigListener.TOPIC, object : LanguageClient.ConfigListener {
override fun configChanged(config: Config) {
state = state.withConfig(config)
project.safeSyncPublisher(Listener.TOPIC)?.stateChanged(state)
}
})
Expand Down
Loading

0 comments on commit 7e476e5

Please sign in to comment.