feat(intellij): update tabby-agent interface and add issue notifications. (#401)

release-0.2
Zhiming Ma 2023-09-05 11:40:46 +08:00 committed by GitHub
parent 3ab365f2c9
commit abfa7975e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 318 additions and 129 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,94 @@
package com.tabbyml.intellijtabby.actions
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.components.service
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 kotlinx.coroutines.launch
class CheckIssueDetail : AnAction() {
private val logger = Logger.getInstance(CheckIssueDetail::class.java)
override fun actionPerformed(e: AnActionEvent) {
val agentService = service<AgentService>()
agentService.issueNotification?.expire()
agentService.scope.launch {
val detail = agentService.getCurrentIssueDetail() ?: return@launch
val serverHealthState = agentService.getServerHealthState()
logger.info("Show issue detail: $detail, $serverHealthState")
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)
invokeLater {
Messages.showInfoMessage(message, title)
}
}
}
private fun buildDetailMessage(detail: Map<String, Any>, serverHealthState: Map<String, Any>?): String {
val stats = detail["completionResponseStats"] as Map<*, *>?
val statsMessages = when (detail["name"]) {
"slowCompletionResponseTime" -> if (stats != null && stats["responses"] is Number && stats["averageResponseTime"] is Number) {
val response = (stats["responses"] as Number).toInt()
val averageResponseTime = (stats["averageResponseTime"] as Number).toInt()
"The average response time of recent $response completion requests is $averageResponseTime ms.\n\n"
} else {
""
}
"highCompletionTimeoutRate" -> if (stats != null && stats["total"] is Number && stats["timeouts"] is Number) {
val timeout = (stats["timeouts"] as Number).toInt()
val total = (stats["total"] as Number).toInt()
"$timeout of $total completion requests timed out.\n\n"
} else {
""
}
else -> ""
}
val device = serverHealthState?.get("device") as String? ?: ""
val model = serverHealthState?.get("model") as String? ?: ""
val helpMessageForRunningLargeModelOnCPU = if (device == "cpu" && model.endsWith("B")) {
"""
Your Tabby server is running model $model on CPU.
This model is too large to run on CPU, please try a smaller model or switch to GPU.
You can find supported model list by search TabbyML on HuggingFace.
"""
} else {
""
}
var helpMessage = ""
if (helpMessageForRunningLargeModelOnCPU.isNotEmpty()) {
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()) {
helpMessage += " - The running model $model is too large to run on your Tabby server. ";
helpMessage += "Please try a smaller model. You can find supported model list by search TabbyML on HuggingFace.\n";
}
return statsMessages + helpMessage
}
override fun update(e: AnActionEvent) {
val agentService = service<AgentService>()
e.presentation.isVisible = agentService.status.value == Agent.Status.ISSUES_EXIST
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}

View File

@ -13,10 +13,13 @@ import com.tabbyml.intellijtabby.agent.AgentService
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
open class OpenAuthPage : AnAction() {
class OpenAuthPage : AnAction() {
private val logger = Logger.getInstance(OpenAuthPage::class.java)
override fun actionPerformed(e: AnActionEvent) {
val agentService = service<AgentService>()
agentService.authNotification?.expire()
val task = object : Task.Modal(
e.project,
"Tabby Server Authorization",
@ -24,7 +27,6 @@ open class OpenAuthPage : AnAction() {
) {
lateinit var job: Job
override fun run(indicator: ProgressIndicator) {
val agentService = service<AgentService>()
job = agentService.scope.launch {
agentService.requestAuth(indicator)
}

View File

@ -13,7 +13,7 @@ class TriggerCompletion : AnAction() {
val completionScheduler = service<CompletionScheduler>()
val editor = e.getRequiredData(CommonDataKeys.EDITOR)
val offset = editor.caretModel.primaryCaret.offset
completionScheduler.schedule(editor, offset, triggerDelay = 0, manually = true)
completionScheduler.schedule(editor, offset, manually = true)
}
override fun update(e: AnActionEvent) {

View File

@ -33,12 +33,15 @@ class Agent : ProcessAdapter() {
READY,
DISCONNECTED,
UNAUTHORIZED,
ISSUES_EXIST,
}
private val statusFlow = MutableStateFlow(Status.NOT_INITIALIZED)
val status = statusFlow.asStateFlow()
private val authRequiredEventFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val authRequiredEvent = authRequiredEventFlow.asSharedFlow()
private val currentIssueFlow = MutableStateFlow<String?>(null)
val currentIssue = currentIssueFlow.asStateFlow()
open class AgentException(message: String) : Exception(message)
@ -81,20 +84,38 @@ class Agent : ProcessAdapter() {
val anonymousUsageTracking: AnonymousUsageTracking? = null,
) {
data class Server(
val endpoint: String,
val endpoint: String? = null,
val requestHeaders: Map<String, String>? = null,
val requestTimeout: Int? = null,
)
data class Completion(
val maxPrefixLines: Int,
val maxSuffixLines: Int,
)
val prompt: Prompt? = null,
val debounce: Debounce? = null,
val timeout: Timeout? = null,
) {
data class Prompt(
val maxPrefixLines: Int? = null,
val maxSuffixLines: Int? = null,
)
data class Debounce(
val mode: String? = null,
val interval: Int? = null,
)
data class Timeout(
val auto: Int? = null,
val manually: Int? = null,
)
}
data class Logs(
val level: String,
val level: String? = null,
)
data class AnonymousUsageTracking(
val disabled: Boolean,
val disabled: Boolean? = null,
)
}
@ -109,8 +130,20 @@ class Agent : ProcessAdapter() {
)
}
suspend fun updateConfig(config: Config): Boolean {
return request("updateConfig", listOf(config))
suspend fun updateConfig(key: String, config: Any): Boolean {
return request("updateConfig", listOf(key, config))
}
suspend fun clearConfig(key: String): Boolean {
return request("clearConfig", listOf(key))
}
suspend fun getIssues(): List<Map<String, Any>> {
return request("getIssues", listOf())
}
suspend fun getServerHealthState(): Map<String, Any>? {
return request("getServerHealthState", listOf())
}
data class CompletionRequest(
@ -118,6 +151,7 @@ class Agent : ProcessAdapter() {
val language: String,
val text: String,
val position: Int,
val manually: Boolean?,
)
data class CompletionResponse(
@ -130,8 +164,16 @@ class Agent : ProcessAdapter() {
)
}
suspend fun getCompletions(request: CompletionRequest): CompletionResponse? {
return request("getCompletions", listOf(request))
suspend fun requestAuthUrl(): AuthUrlResponse? {
return request("requestAuthUrl", listOf())
}
suspend fun waitForAuthToken(code: String) {
return request("waitForAuthToken", listOf(code))
}
suspend fun provideCompletions(request: CompletionRequest): CompletionResponse? {
return request("provideCompletions", listOf(request))
}
data class LogEventRequest(
@ -148,7 +190,7 @@ class Agent : ProcessAdapter() {
}
}
suspend fun postEvent(event: LogEventRequest): Boolean {
suspend fun postEvent(event: LogEventRequest) {
return request("postEvent", listOf(event))
}
@ -157,14 +199,6 @@ class Agent : ProcessAdapter() {
val code: String,
)
suspend fun requestAuthUrl(): AuthUrlResponse? {
return request("requestAuthUrl", listOf())
}
suspend fun waitForAuthToken(code: String) {
return request("waitForAuthToken", listOf(code))
}
fun close() {
streamWriter.close()
process.killProcess()
@ -245,8 +279,12 @@ class Agent : ProcessAdapter() {
"ready" -> Status.READY
"disconnected" -> Status.DISCONNECTED
"unauthorized" -> Status.UNAUTHORIZED
"issuesExist" -> Status.ISSUES_EXIST
else -> Status.NOT_INITIALIZED
}
if (statusFlow.value !== Status.ISSUES_EXIST) {
currentIssueFlow.value = null
}
}
"configUpdated" -> {
@ -258,6 +296,11 @@ class Agent : ProcessAdapter() {
authRequiredEventFlow.tryEmit(Unit)
}
"newIssue" -> {
logger.info("Agent notification $event")
currentIssueFlow.value = (event["issue"] as Map<*, *>)["name"] as String?
}
else -> {
logger.error("Agent notification, unknown event name: ${event["event"]}")
}

View File

@ -7,6 +7,7 @@ import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.notification.Notifications
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.ReadAction
@ -34,7 +35,12 @@ class AgentService : Disposable {
private val logger = Logger.getInstance(AgentService::class.java)
private var agent: Agent = Agent()
val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
var authNotification: Notification? = null
private set
var issueNotification: Notification? = null
private set
val status get() = agent.status
val currentIssue get() = agent.currentIssue
init {
val settings = service<ApplicationSettingsState>()
@ -51,21 +57,26 @@ class AgentService : Disposable {
logger.info("Agent init done.")
} catch (e: Exception) {
logger.error("Agent init failed: $e")
anonymousUsageLogger.event("IntelliJInitFailed", mapOf(
"client" to client,
"error" to e.stackTraceToString()
))
anonymousUsageLogger.event(
"IntelliJInitFailed", mapOf(
"client" to client, "error" to e.stackTraceToString()
)
)
}
}
scope.launch {
settings.state.collect {
updateConfig(createAgentConfig(it))
if (it.serverEndpoint.isNotBlank()) {
updateConfig("server.endpoint", it.serverEndpoint)
} else {
clearConfig("server.endpoint")
}
updateConfig("anonymousUsageTracking.disable", it.isAnonymousUsageTrackingDisabled)
}
}
scope.launch {
logger.info("Add authRequired event listener.")
agent.authRequiredEvent.collect {
logger.info("Will show auth required notification.")
val notification = Notification(
@ -73,18 +84,31 @@ class AgentService : Disposable {
"Authorization required for Tabby server",
NotificationType.WARNING,
)
notification.addAction(object : OpenAuthPage() {
init {
getTemplatePresentation().text = "Open Authorization Page..."
getTemplatePresentation().description = "Open the authorization web page in your web browser."
}
override fun actionPerformed(e: AnActionEvent) {
notification.expire()
super.actionPerformed(e)
}
})
notification.addAction(ActionManager.getInstance().getAction("Tabby.OpenAuthPage"))
invokeLater {
authNotification?.expire()
authNotification = notification
Notifications.Bus.notify(notification)
}
}
}
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 notification = Notification(
"com.tabbyml.intellijtabby.notification.warning",
content,
NotificationType.WARNING,
)
notification.addAction(ActionManager.getInstance().getAction("Tabby.CheckIssueDetail"))
invokeLater {
issueNotification?.expire()
issueNotification = notification
Notifications.Bus.notify(notification)
}
}
@ -114,24 +138,30 @@ class AgentService : Disposable {
agent.status.first { it != Agent.Status.NOT_INITIALIZED }
}
private suspend fun updateConfig(config: Agent.Config) {
private suspend fun updateConfig(key: String, config: Any) {
waitForInitialized()
agent.updateConfig(config)
agent.updateConfig(key, config)
}
suspend fun getCompletion(editor: Editor, offset: Int): Agent.CompletionResponse? {
private suspend fun clearConfig(key: String) {
waitForInitialized()
agent.clearConfig(key)
}
suspend fun provideCompletion(editor: Editor, offset: Int, manually: Boolean = false): Agent.CompletionResponse? {
waitForInitialized()
return ReadAction.compute<PsiFile, Throwable> {
editor.project?.let { project ->
PsiDocumentManager.getInstance(project).getPsiFile(editor.document)
}
}?.let { file ->
agent.getCompletions(
agent.provideCompletions(
Agent.CompletionRequest(
file.virtualFile.path,
file.getLanguageId(),
editor.document.text,
offset
offset,
manually,
)
)
}
@ -166,16 +196,26 @@ class AgentService : Disposable {
}
} else {
Notification(
"com.tabbyml.intellijtabby.notification.info",
"You are already authorized.",
NotificationType.INFORMATION
"com.tabbyml.intellijtabby.notification.info", "You are already authorized.", NotificationType.INFORMATION
)
}
invokeLater {
authNotification?.expire()
authNotification = notification
Notifications.Bus.notify(notification)
}
}
suspend fun getCurrentIssueDetail(): Map<String, Any>? {
waitForInitialized()
return agent.getIssues().firstOrNull { it["name"] == currentIssue.value }
}
suspend fun getServerHealthState(): Map<String, Any>? {
waitForInitialized()
return agent.getServerHealthState()
}
override fun dispose() {
agent.close()
}
@ -183,15 +223,16 @@ 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")
if (this.language != Language.ANY && 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")
return this.language.id.toLowerCasePreservingASCIIRules().replace("#", "sharp").replace("++", "pp")
.replace(" ", "")
}
return if (filetypeMap.containsKey(this.fileType.defaultExtension)) {

View File

@ -7,7 +7,6 @@ import com.intellij.openapi.editor.Editor
import com.tabbyml.intellijtabby.agent.AgentService
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Service
@ -19,7 +18,7 @@ class CompletionScheduler {
var scheduled: CompletionContext? = null
private set
fun schedule(editor: Editor, offset: Int, triggerDelay: Long = 150, manually: Boolean = false) {
fun schedule(editor: Editor, offset: Int, manually: Boolean = false) {
val agentService = service<AgentService>()
val inlineCompletionService = service<InlineCompletionService>()
val settings = service<ApplicationSettingsState>()
@ -28,14 +27,10 @@ class CompletionScheduler {
if (!manually && !settings.isAutoCompletionEnabled) {
return@launch
}
logger.info("Schedule completion at $offset after $triggerDelay ms.")
delay(triggerDelay)
if (!manually && !settings.isAutoCompletionEnabled) {
return@launch
}
logger.info("Trigger completion at $offset")
agentService.getCompletion(editor, offset)?.let {
agentService.provideCompletion(editor, offset, manually)?.let {
logger.info("Show completion at $offset: $it")
inlineCompletionService.show(editor, offset, it)
}
}

View File

@ -40,10 +40,10 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
val settings = service<ApplicationSettingsState>()
val agentService = service<AgentService>()
updateStatusScope.launch {
settings.state.combine(agentService.status) { settings, agentStatus ->
Pair(settings, agentStatus)
combine(settings.state, agentService.status, agentService.currentIssue) { settings, agentStatus, currentIssue ->
Triple(settings, agentStatus, currentIssue)
}.collect {
updateStatus(it.first, it.second)
updateStatus(it.first, it.second, it.third)
}
}
}
@ -73,6 +73,7 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
val actionManager = ActionManager.getInstance()
return arrayOf(
actionManager.getAction("Tabby.OpenAuthPage"),
actionManager.getAction("Tabby.CheckIssueDetail"),
actionManager.getAction("Tabby.ToggleAutoCompletionEnabled"),
actionManager.getAction("Tabby.OpenSettings"),
)
@ -85,7 +86,7 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
)
}
private fun updateStatus(settingsState: ApplicationSettingsState.State, agentStatus: Agent.Status) {
private fun updateStatus(settingsState: ApplicationSettingsState.State, agentStatus: Agent.Status, currentIssue: String?) {
if (!settingsState.isAutoCompletionEnabled) {
icon = AllIcons.Windows.CloseSmall
tooltip = "Tabby: Auto completion is disabled"
@ -107,6 +108,14 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
icon = AllIcons.General.Warning
tooltip = "Tabby: Requires authorization"
}
Agent.Status.ISSUES_EXIST -> {
icon = AllIcons.General.Warning
tooltip = when(currentIssue) {
"slowCompletionResponseTime" -> "Tabby: Completion requests appear to take too much time"
"highCompletionTimeoutRate" -> "Tabby: Most completion requests timed out"
else -> "Tabby: Issues exist"
}
}
}
}
invokeLater {

View File

@ -88,6 +88,11 @@
text="Open Authorization Page..."
description="Open the authorization web page in your web browser.">
</action>
<action id="Tabby.CheckIssueDetail"
class="com.tabbyml.intellijtabby.actions.CheckIssueDetail"
text="Check Issue Detail..."
description="Show detail information for current issue.">
</action>
<action id="Tabby.ToggleAutoCompletionEnabled"
class="com.tabbyml.intellijtabby.actions.ToggleAutoCompletionEnabled">
</action>