feat(intellij): update tabby-agent to 0.3.1. (#490)

release-0.2
Zhiming Ma 2023-09-29 18:06:47 +08:00 committed by GitHub
parent 4ebad71805
commit 52c4ef38d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 322 additions and 197 deletions

View File

@ -24,7 +24,7 @@ jobs:
distribution: zulu
java-version: 17
- name: Test Build
uses: gradle/gradle-build-action@v2
uses: gradle/gradle-build-action@v2.4.2
with:
arguments: buildPlugin
build-root-directory: clients/intellij

View File

@ -6,7 +6,7 @@ plugins {
}
group = "com.tabbyml"
version = "0.5.0"
version = "0.6.0-dev"
repositories {
mavenCentral()

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,6 @@ 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
@ -89,7 +88,7 @@ class CheckIssueDetail : AnAction() {
override fun update(e: AnActionEvent) {
val agentService = service<AgentService>()
e.presentation.isVisible = agentService.status.value == Agent.Status.ISSUES_EXIST
e.presentation.isVisible = agentService.currentIssue.value != null
}
override fun getActionUpdateThread(): ActionUpdateThread {

View File

@ -1,29 +0,0 @@
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.components.service
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
class ToggleAutoCompletionEnabled : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val settings = service<ApplicationSettingsState>()
settings.isAutoCompletionEnabled = !settings.isAutoCompletionEnabled
}
override fun update(e: AnActionEvent) {
val settings = service<ApplicationSettingsState>()
if (settings.isAutoCompletionEnabled) {
e.presentation.text = "Disable Auto Completion"
e.presentation.description = "Tabby does not show completion suggestions automatically, you can still request them on demand."
} else {
e.presentation.text = "Enable Auto Completion"
e.presentation.description = "Tabby shows inline completion suggestions automatically."
}
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}

View File

@ -0,0 +1,32 @@
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.components.service
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
class ToggleInlineCompletionTriggerMode : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val settings = service<ApplicationSettingsState>()
settings.completionTriggerMode = when (settings.completionTriggerMode) {
ApplicationSettingsState.TriggerMode.AUTOMATIC -> ApplicationSettingsState.TriggerMode.MANUAL
ApplicationSettingsState.TriggerMode.MANUAL -> ApplicationSettingsState.TriggerMode.AUTOMATIC
}
}
override fun update(e: AnActionEvent) {
val settings = service<ApplicationSettingsState>()
if (settings.completionTriggerMode == ApplicationSettingsState.TriggerMode.AUTOMATIC) {
e.presentation.text = "Switch to Manual Mode"
e.presentation.description = "Manual trigger inline completion suggestions by pressing `Alt + \\`."
} else {
e.presentation.text = "Switch to Automatic Mode"
e.presentation.description = "Show inline completion suggestions automatically."
}
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}

View File

@ -5,15 +5,15 @@ import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.components.service
import com.tabbyml.intellijtabby.editor.CompletionScheduler
import com.tabbyml.intellijtabby.editor.CompletionProvider
class TriggerCompletion : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val completionScheduler = service<CompletionScheduler>()
val completionScheduler = service<CompletionProvider>()
val editor = e.getRequiredData(CommonDataKeys.EDITOR)
val offset = editor.caretModel.primaryCaret.offset
completionScheduler.schedule(editor, offset, manually = true)
completionScheduler.provideCompletion(editor, offset, manually = true)
}
override fun update(e: AnActionEvent) {

View File

@ -35,7 +35,6 @@ class Agent : ProcessAdapter() {
READY,
DISCONNECTED,
UNAUTHORIZED,
ISSUES_EXIST,
}
private val statusFlow = MutableStateFlow(Status.NOT_INITIALIZED)
@ -140,17 +139,30 @@ class Agent : ProcessAdapter() {
)
}
suspend fun initialize(config: Config, client: String): Boolean {
data class ClientProperties(
val user: Map<String, Any>,
val session: Map<String, Any>,
)
suspend fun initialize(config: Config, clientProperties: ClientProperties): Boolean {
return request(
"initialize", listOf(
mapOf(
"config" to config,
"client" to client,
"clientProperties" to clientProperties,
)
)
)
}
suspend fun finalize(): Boolean {
return request("finalize", listOf())
}
suspend fun updateClientProperties(type: String, key: String, value: Any): Boolean {
return request("updateClientProperties", listOf(type, key, value))
}
suspend fun updateConfig(key: String, config: Any): Boolean {
return request("updateConfig", listOf(key, config))
}
@ -159,14 +171,44 @@ class Agent : ProcessAdapter() {
return request("clearConfig", listOf(key))
}
suspend fun getIssues(): List<Map<String, Any>> {
suspend fun getConfig(): Config {
return request("getConfig", listOf())
}
suspend fun getStatus(): Status {
return request("getStatus", listOf())
}
suspend fun getIssues(): List<String> {
return request("getIssues", listOf())
}
data class GetIssueDetailOptions(
val index: Int? = null,
val name: String? = null,
)
suspend fun getIssueDetail(options: GetIssueDetailOptions): Map<String, Any>? {
return request("getIssueDetail", listOf(options))
}
suspend fun getServerHealthState(): Map<String, Any>? {
return request("getServerHealthState", listOf())
}
data class AuthUrlResponse(
val authUrl: String,
val code: String,
)
suspend fun requestAuthUrl(): AuthUrlResponse? {
return request("requestAuthUrl", listOf(ABORT_SIGNAL_ENABLED))
}
suspend fun waitForAuthToken(code: String) {
return request("waitForAuthToken", listOf(code, ABORT_SIGNAL_ENABLED))
}
data class CompletionRequest(
val filepath: String,
val language: String,
@ -185,14 +227,6 @@ class Agent : ProcessAdapter() {
)
}
suspend fun requestAuthUrl(): AuthUrlResponse? {
return request("requestAuthUrl", listOf(ABORT_SIGNAL_ENABLED))
}
suspend fun waitForAuthToken(code: String) {
return request("waitForAuthToken", listOf(code, ABORT_SIGNAL_ENABLED))
}
suspend fun provideCompletions(request: CompletionRequest): CompletionResponse? {
return request("provideCompletions", listOf(request, ABORT_SIGNAL_ENABLED))
}
@ -201,6 +235,7 @@ class Agent : ProcessAdapter() {
val type: EventType,
@SerializedName("completion_id") val completionId: String,
@SerializedName("choice_index") val choiceIndex: Int,
@SerializedName("select_kind") val selectKind: SelectKind? = null,
) {
enum class EventType {
@SerializedName("view")
@ -209,16 +244,17 @@ class Agent : ProcessAdapter() {
@SerializedName("select")
SELECT,
}
enum class SelectKind {
@SerializedName("line")
LINE,
}
}
suspend fun postEvent(event: LogEventRequest) {
request<Any>("postEvent", listOf(event, ABORT_SIGNAL_ENABLED))
}
data class AuthUrlResponse(
val authUrl: String,
val code: String,
)
fun close() {
try {
@ -304,12 +340,8 @@ 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" -> {
@ -321,9 +353,9 @@ class Agent : ProcessAdapter() {
authRequiredEventFlow.tryEmit(Unit)
}
"newIssue" -> {
"issuesUpdated" -> {
logger.info("Agent notification $event")
currentIssueFlow.value = (event["issue"] as Map<*, *>)["name"] as String?
currentIssueFlow.value = (event["issues"] as List<*>).firstOrNull() as String?
}
else -> {

View File

@ -26,22 +26,27 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@Service
class AgentService : Disposable {
private val logger = Logger.getInstance(AgentService::class.java)
private var agent: Agent = Agent()
val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
private var initFailedNotification: Notification? = null
var authNotification: Notification? = null
private set
var issueNotification: Notification? = null
private set
private var completionResponseWarningShown = false
enum class Status {
INITIALIZING,
INITIALIZATION_FAILED,
}
private var initResultFlow: MutableStateFlow<Boolean?> = MutableStateFlow(null)
val status = initResultFlow.combine(agent.status) { initResult, agentStatus ->
if (initResult == null) {
@ -59,14 +64,12 @@ class AgentService : Disposable {
val settings = service<ApplicationSettingsState>()
val anonymousUsageLogger = service<AnonymousUsageLogger>()
scope.launch {
val appInfo = ApplicationInfo.getInstance().fullApplicationName
val pluginId = "com.tabbyml.intellij-tabby"
val pluginVersion = PluginManagerCore.getPlugin(PluginId.getId(pluginId))?.version
val client = "$appInfo $pluginId $pluginVersion"
val config = createAgentConfig(settings.data)
val clientProperties = createClientProperties(settings.data)
try {
agent.open()
agent.initialize(createAgentConfig(settings.data), client)
agent.initialize(config, clientProperties)
initResultFlow.value = true
logger.info("Agent init done.")
} catch (e: Exception) {
@ -74,7 +77,7 @@ class AgentService : Disposable {
logger.warn("Agent init failed: $e")
anonymousUsageLogger.event(
"IntelliJInitFailed", mapOf(
"client" to client, "error" to e.stackTraceToString()
"client" to clientProperties.session["client"] as String, "error" to e.stackTraceToString()
)
)
val notification = Notification(
@ -99,6 +102,7 @@ class AgentService : Disposable {
} else {
clearConfig("server.endpoint")
}
updateClientProperties("user", "intellij.triggerMode", it.completionTriggerMode)
updateConfig("anonymousUsageTracking.disable", it.isAnonymousUsageTrackingDisabled)
}
}
@ -127,6 +131,10 @@ class AgentService : Disposable {
"highCompletionTimeoutRate" -> "Most completion requests timed out"
else -> return@collect
}
if (completionResponseWarningShown) {
return@collect
}
completionResponseWarningShown = true
val notification = Notification(
"com.tabbyml.intellijtabby.notification.warning",
content,
@ -161,10 +169,36 @@ class AgentService : Disposable {
)
}
private fun createClientProperties(state: ApplicationSettingsState.State): Agent.ClientProperties {
val appInfo = ApplicationInfo.getInstance()
val appVersion = appInfo.fullVersion
val appName = appInfo.fullApplicationName.replace(appVersion, "").trim()
val pluginId = "com.tabbyml.intellij-tabby"
val pluginVersion = PluginManagerCore.getPlugin(PluginId.getId(pluginId))?.version
val client = "$appName $pluginId $pluginVersion"
return Agent.ClientProperties(
user = mapOf(
"intellij" to mapOf(
"triggerMode" to state.completionTriggerMode,
),
),
session = mapOf(
"client" to client,
"ide" to mapOf("name" to appName, "version" to appVersion),
"tabby_plugin" to mapOf("name" to pluginId, "version" to pluginVersion),
),
)
}
private suspend fun waitForInitialized() {
agent.status.first { it != Agent.Status.NOT_INITIALIZED }
}
private suspend fun updateClientProperties(type: String, key: String, config: Any) {
waitForInitialized()
agent.updateClientProperties(type, key, config)
}
private suspend fun updateConfig(key: String, config: Any) {
waitForInitialized()
agent.updateConfig(key, config)
@ -235,7 +269,7 @@ class AgentService : Disposable {
suspend fun getCurrentIssueDetail(): Map<String, Any>? {
waitForInitialized()
return agent.getIssues().firstOrNull { it["name"] == currentIssue.value }
return agent.getIssueDetail(Agent.GetIssueDetailOptions(name = currentIssue.value))
}
suspend fun getServerHealthState(): Map<String, Any>? {
@ -244,6 +278,11 @@ class AgentService : Disposable {
}
override fun dispose() {
runBlocking {
runCatching {
agent.finalize()
}
}
agent.close()
}

View File

@ -5,44 +5,41 @@ import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
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.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@Service
class CompletionScheduler {
private val logger = Logger.getInstance(CompletionScheduler::class.java)
class CompletionProvider {
private val logger = Logger.getInstance(CompletionProvider::class.java)
data class CompletionContext(val editor: Editor, val offset: Int, val job: Job)
var scheduled: CompletionContext? = null
private set
private val ongoingCompletionFlow: MutableStateFlow<CompletionContext?> = MutableStateFlow(null)
val ongoingCompletion = ongoingCompletionFlow.asStateFlow()
fun schedule(editor: Editor, offset: Int, manually: Boolean = false) {
fun provideCompletion(editor: Editor, offset: Int, manually: Boolean = false) {
val agentService = service<AgentService>()
val inlineCompletionService = service<InlineCompletionService>()
val settings = service<ApplicationSettingsState>()
clear()
val job = agentService.scope.launch {
if (!manually && !settings.isAutoCompletionEnabled) {
return@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)
ongoingCompletionFlow.value = null
}
}
scheduled = CompletionContext(editor, offset, job)
ongoingCompletionFlow.value = CompletionContext(editor, offset, job)
}
fun clear() {
val inlineCompletionService = service<InlineCompletionService>()
inlineCompletionService.dismiss()
scheduled?.let {
ongoingCompletionFlow.value?.let {
it.job.cancel()
scheduled = null
ongoingCompletionFlow.value = null
}
}
}

View File

@ -8,6 +8,7 @@ import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.util.messages.MessageBusConnection
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
class EditorListener : EditorFactoryListener {
private val logger = Logger.getInstance(EditorListener::class.java)
@ -16,14 +17,17 @@ class EditorListener : EditorFactoryListener {
override fun editorCreated(event: EditorFactoryEvent) {
val editor = event.editor
val editorManager = editor.project?.let { FileEditorManager.getInstance(it) } ?: return
val completionScheduler = service<CompletionScheduler>()
val settings = service<ApplicationSettingsState>()
val completionProvider = service<CompletionProvider>()
editor.caretModel.addCaretListener(object : CaretListener {
override fun caretPositionChanged(event: CaretEvent) {
if (editorManager.selectedTextEditor == editor) {
completionScheduler.scheduled?.let {
if (it.editor != editor || it.offset != editor.caretModel.primaryCaret.offset) {
completionScheduler.clear()
completionProvider.ongoingCompletion.value.let {
if (it != null && it.editor == editor && it.offset == editor.caretModel.primaryCaret.offset) {
// keep ongoing completion
} else {
completionProvider.clear()
}
}
}
@ -33,8 +37,10 @@ class EditorListener : EditorFactoryListener {
editor.document.addDocumentListener(object : DocumentListener {
override fun documentChanged(event: DocumentEvent) {
if (editorManager.selectedTextEditor == editor) {
val offset = event.offset + event.newFragment.length
completionScheduler.schedule(editor, offset)
if (settings.completionTriggerMode == ApplicationSettingsState.TriggerMode.AUTOMATIC) {
val offset = event.offset + event.newFragment.length
completionProvider.provideCompletion(editor, offset)
}
}
}
})
@ -42,10 +48,10 @@ class EditorListener : EditorFactoryListener {
editor.project?.messageBus?.connect()?.let {
it.subscribe(
FileEditorManagerListener.FILE_EDITOR_MANAGER,
object: FileEditorManagerListener {
object : FileEditorManagerListener {
override fun selectionChanged(event: FileEditorManagerEvent) {
logger.info("FileEditorManagerListener selectionChanged.")
completionScheduler.clear()
completionProvider.clear()
}
}
)
@ -54,9 +60,6 @@ class EditorListener : EditorFactoryListener {
}
override fun editorReleased(event: EditorFactoryEvent) {
messagesConnection[event.editor]?.let {
it.disconnect()
it.dispose()
}
messagesConnection[event.editor]?.disconnect()
}
}

View File

@ -18,21 +18,21 @@ class ApplicationConfigurable : Configurable {
override fun isModified(): Boolean {
val settings = service<ApplicationSettingsState>()
return settingsPanel.isAutoCompletionEnabled != settings.isAutoCompletionEnabled
return settingsPanel.completionTriggerMode != settings.completionTriggerMode
|| settingsPanel.serverEndpoint != settings.serverEndpoint
|| settingsPanel.isAnonymousUsageTrackingDisabled != settings.isAnonymousUsageTrackingDisabled
}
override fun apply() {
val settings = service<ApplicationSettingsState>()
settings.isAutoCompletionEnabled = settingsPanel.isAutoCompletionEnabled
settings.completionTriggerMode = settingsPanel.completionTriggerMode
settings.serverEndpoint = settingsPanel.serverEndpoint
settings.isAnonymousUsageTrackingDisabled = settingsPanel.isAnonymousUsageTrackingDisabled
}
override fun reset() {
val settings = service<ApplicationSettingsState>()
settingsPanel.isAutoCompletionEnabled = settings.isAutoCompletionEnabled
settingsPanel.completionTriggerMode = settings.completionTriggerMode
settingsPanel.serverEndpoint = settings.serverEndpoint
settingsPanel.isAnonymousUsageTrackingDisabled = settings.isAnonymousUsageTrackingDisabled
}

View File

@ -1,30 +1,62 @@
package com.tabbyml.intellijtabby.settings
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBRadioButton
import com.intellij.ui.components.JBTextField
import com.intellij.util.ui.FormBuilder
import javax.swing.ButtonGroup
import javax.swing.JPanel
class ApplicationSettingsPanel {
private val isAutoCompletionEnabledCheckBox = JBCheckBox("Enable auto completion")
private val serverEndpointTextField = JBTextField()
private val isAnonymousUsageTrackingDisabledCheckBox = JBCheckBox("Disable anonymous usage tracking")
private val serverEndpointPanel = FormBuilder.createFormBuilder()
.addComponent(serverEndpointTextField)
.addTooltip(
"""
<html>
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/>
Default to <i>http://localhost:8080</i>.
</html>
""".trimIndent()
)
.panel
private val completionTriggerModeAutomaticRadioButton = JBRadioButton("Automatic")
private val completionTriggerModeManualRadioButton = JBRadioButton("Manual")
private val completionTriggerModeRadioGroup = ButtonGroup().apply {
add(completionTriggerModeAutomaticRadioButton)
add(completionTriggerModeManualRadioButton)
}
private val completionTriggerModePanel: JPanel = FormBuilder.createFormBuilder()
.addComponent(completionTriggerModeAutomaticRadioButton)
.addTooltip("Trigger automatically when you stop typing")
.addComponent(completionTriggerModeManualRadioButton)
.addTooltip("Trigger manually by pressing `Alt + \\`")
.panel
private val isAnonymousUsageTrackingDisabledCheckBox = JBCheckBox("Disable")
val mainPanel: JPanel = FormBuilder.createFormBuilder()
.addLabeledComponent("Server endpoint", serverEndpointTextField, 1, false)
.addTooltip("A http or https URL of Tabby server endpoint.")
.addTooltip("If leave empty, server endpoint config in `~/.tabby-client/agent/config.toml` will be used")
.addTooltip("Default to 'http://localhost:8080'.")
.addSeparator()
.addComponent(isAutoCompletionEnabledCheckBox, 1)
.addComponent(isAnonymousUsageTrackingDisabledCheckBox, 1)
.addLabeledComponent("Server endpoint", serverEndpointPanel, 5, false)
.addSeparator(5)
.addLabeledComponent("Inline completion trigger", completionTriggerModePanel, 5, false)
.addSeparator(5)
.addLabeledComponent("Anonymous usage tracking", isAnonymousUsageTrackingDisabledCheckBox, 5, false)
.addComponentFillVertically(JPanel(), 0)
.panel
var isAutoCompletionEnabled: Boolean
get() = isAutoCompletionEnabledCheckBox.isSelected
var completionTriggerMode: ApplicationSettingsState.TriggerMode
get() = if (completionTriggerModeAutomaticRadioButton.isSelected) {
ApplicationSettingsState.TriggerMode.AUTOMATIC
} else {
ApplicationSettingsState.TriggerMode.MANUAL
}
set(value) {
isAutoCompletionEnabledCheckBox.isSelected = value
when (value) {
ApplicationSettingsState.TriggerMode.AUTOMATIC -> completionTriggerModeAutomaticRadioButton.isSelected = true
ApplicationSettingsState.TriggerMode.MANUAL -> completionTriggerModeManualRadioButton.isSelected = true
}
}
var serverEndpoint: String

View File

@ -1,5 +1,6 @@
package com.tabbyml.intellijtabby.settings
import com.google.gson.annotations.SerializedName
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
@ -14,7 +15,14 @@ import kotlinx.coroutines.flow.asStateFlow
storages = [Storage("intellij-tabby.xml")]
)
class ApplicationSettingsState : PersistentStateComponent<ApplicationSettingsState> {
var isAutoCompletionEnabled: Boolean = true
enum class TriggerMode {
@SerializedName("manual")
MANUAL,
@SerializedName("automatic")
AUTOMATIC,
}
var completionTriggerMode: TriggerMode = TriggerMode.AUTOMATIC
set(value) {
field = value
stateFlow.value = this.data
@ -31,14 +39,14 @@ class ApplicationSettingsState : PersistentStateComponent<ApplicationSettingsSta
}
data class State(
val isAutoCompletionEnabled: Boolean,
val completionTriggerMode: TriggerMode,
val serverEndpoint: String,
val isAnonymousUsageTrackingDisabled: Boolean,
)
val data: State
get() = State(
isAutoCompletionEnabled = isAutoCompletionEnabled,
completionTriggerMode = completionTriggerMode,
serverEndpoint = serverEndpoint,
isAnonymousUsageTrackingDisabled = isAnonymousUsageTrackingDisabled,
)

View File

@ -11,14 +11,17 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.wm.StatusBarWidget
import com.intellij.openapi.wm.impl.status.EditorBasedStatusBarPopup
import com.intellij.openapi.wm.impl.status.widget.StatusBarEditorBasedWidgetFactory
import com.intellij.ui.AnimatedIcon
import com.tabbyml.intellijtabby.agent.Agent
import com.tabbyml.intellijtabby.agent.AgentService
import com.tabbyml.intellijtabby.editor.CompletionProvider
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import javax.swing.Icon
class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
override fun getId(): String {
@ -30,20 +33,28 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
}
override fun createWidget(project: Project): StatusBarWidget {
data class CombinedState(
val settings: ApplicationSettingsState.State,
val agentStatus: Enum<*>,
val currentIssue: String?,
val ongoingCompletion: CompletionProvider.CompletionContext?,
)
return object : EditorBasedStatusBarPopup(project, false) {
val updateStatusScope: CoroutineScope = CoroutineScope(Dispatchers.Main)
val text = "Tabby"
var icon = AllIcons.Actions.Refresh
var icon: Icon = AnimatedIcon.Default()
var tooltip = "Tabby: Initializing"
init {
val settings = service<ApplicationSettingsState>()
val agentService = service<AgentService>()
val completionProvider = service<CompletionProvider>()
updateStatusScope.launch {
combine(settings.state, agentService.status, agentService.currentIssue) { settings, agentStatus, currentIssue ->
Triple(settings, agentStatus, currentIssue)
combine(settings.state, agentService.status, agentService.currentIssue, completionProvider.ongoingCompletion) { settings, agentStatus, currentIssue, ongoingCompletion ->
CombinedState(settings, agentStatus, currentIssue, ongoingCompletion)
}.collect {
updateStatus(it.first, it.second, it.third)
updateStatus(it)
}
}
}
@ -74,7 +85,7 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
return arrayOf(
actionManager.getAction("Tabby.OpenAuthPage"),
actionManager.getAction("Tabby.CheckIssueDetail"),
actionManager.getAction("Tabby.ToggleAutoCompletionEnabled"),
actionManager.getAction("Tabby.ToggleInlineCompletionTriggerMode"),
actionManager.getAction("Tabby.OpenSettings"),
)
}
@ -86,41 +97,51 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
)
}
private fun updateStatus(settingsState: ApplicationSettingsState.State, agentStatus: Enum<*>, currentIssue: String?) {
if (!settingsState.isAutoCompletionEnabled) {
icon = AllIcons.Windows.CloseSmall
tooltip = "Tabby: Auto completion is disabled"
} else {
when(agentStatus) {
AgentService.Status.INITIALIZING, Agent.Status.NOT_INITIALIZED -> {
icon = AllIcons.Actions.Refresh
tooltip = "Tabby: Initializing"
}
AgentService.Status.INITIALIZATION_FAILED -> {
icon = AllIcons.General.Error
tooltip = "Tabby: Initialization failed"
}
Agent.Status.READY -> {
icon = AllIcons.Actions.Checked
tooltip = "Tabby: Ready"
}
Agent.Status.DISCONNECTED -> {
icon = AllIcons.General.Error
tooltip = "Tabby: Cannot connect to Server"
}
Agent.Status.UNAUTHORIZED -> {
private fun updateStatus(state: CombinedState) {
when(state.agentStatus) {
AgentService.Status.INITIALIZING, Agent.Status.NOT_INITIALIZED -> {
icon = AnimatedIcon.Default()
tooltip = "Tabby: Initializing"
}
AgentService.Status.INITIALIZATION_FAILED -> {
icon = AllIcons.General.Error
tooltip = "Tabby: Initialization failed"
}
Agent.Status.READY -> {
if (state.currentIssue != null) {
icon = AllIcons.General.Warning
tooltip = "Tabby: Requires authorization"
}
Agent.Status.ISSUES_EXIST -> {
icon = AllIcons.General.Warning
tooltip = when(currentIssue) {
tooltip = when(state.currentIssue) {
"slowCompletionResponseTime" -> "Tabby: Completion requests appear to take too much time"
"highCompletionTimeoutRate" -> "Tabby: Most completion requests timed out"
else -> "Tabby: Issues exist"
}
} else {
when (state.settings.completionTriggerMode) {
ApplicationSettingsState.TriggerMode.AUTOMATIC -> {
icon = AllIcons.Actions.Checked
tooltip = "Tabby: Automatic code completion is enabled"
}
ApplicationSettingsState.TriggerMode.MANUAL -> {
if (state.ongoingCompletion == null) {
icon = AllIcons.General.ChevronRight
tooltip = "Tabby: Standing by, press `Alt + \\` to trigger code completion."
} else {
icon = AnimatedIcon.Default()
tooltip = "Tabby: Generating code completions"
}
}
}
}
}
Agent.Status.DISCONNECTED -> {
icon = AllIcons.General.Error
tooltip = "Tabby: Cannot connect to Server, please check settings"
}
Agent.Status.UNAUTHORIZED -> {
icon = AllIcons.General.Warning
tooltip = "Tabby: Authorization required, click to continue"
}
}
invokeLater {
update { myStatusBar?.updateWidget(ID()) }

View File

@ -22,17 +22,7 @@
<h2 id="demo">Demo</h2>
<p>Try our online demo <a href="https://tabby.tabbyml.com/playground/">here</a>.</p>
<h2 id="requirements">Requirements</h2>
Tabby plugin requires <a href="https://nodejs.org/">Node.js</a> 16.0+ installed and added into <code>PATH</code> enviroment variable. </p>
<h2 id="get-started">Get Started</h2>
<ol>
<li>
Set up the Tabby server: you can get a Tabby Cloud hosted server <a href="https://app.tabbyml.com">here</a>, or build your self-hosted Tabby server following <a href="https://tabby.tabbyml.com/docs/installation/">this guide</a>.<br/>
<b>Note:</b> Tabby Cloud is currently in beta. Join our <a href="https://join.slack.com/t/tabbycommunity/shared_invite/zt-1xeiddizp-bciR2RtFTaJ37RBxr8VxpA">Slack community</a> and ask in Tabby Cloud channel to get a beta invite.
</li>
<li>Open the settings page <code>Settings &gt; Editor &gt; Tabby</code>, or click the <code>Tabby</code> status bar item and <code>Open Settings...</code>. Fill in the server endpoint URL to connect the plugin to your Tabby server. The status bar item will show a checked icon if the connection is successful.</li>
<li>Once setup is complete, Tabby will provide inline suggestions automatically, and you can accept suggestions by just pressing the <code>Tab</code> key.</li>
<li>You can find more actions and hotkey in the IDE tools menu <code>Code &gt; Tabby</code>.</li>
</ol>
Tabby plugin requires <a href="https://nodejs.org/">Node.js</a> v18+ installed and added into <code>PATH</code> environment variable. </p>
]]></description>
<!-- Product and plugin compatibility requirements.
@ -93,8 +83,8 @@
text="Check Issue Detail..."
description="Show detail information for current issue.">
</action>
<action id="Tabby.ToggleAutoCompletionEnabled"
class="com.tabbyml.intellijtabby.actions.ToggleAutoCompletionEnabled">
<action id="Tabby.ToggleInlineCompletionTriggerMode"
class="com.tabbyml.intellijtabby.actions.ToggleInlineCompletionTriggerMode">
</action>
<action id="Tabby.OpenSettings"
class="com.tabbyml.intellijtabby.actions.OpenSettings"

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 107 KiB