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 distribution: zulu
java-version: 17 java-version: 17
- name: Test Build - name: Test Build
uses: gradle/gradle-build-action@v2 uses: gradle/gradle-build-action@v2.4.2
with: with:
arguments: buildPlugin arguments: buildPlugin
build-root-directory: clients/intellij build-root-directory: clients/intellij

View File

@ -6,7 +6,7 @@ plugins {
} }
group = "com.tabbyml" group = "com.tabbyml"
version = "0.5.0" version = "0.6.0-dev"
repositories { repositories {
mavenCentral() 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.components.service
import com.intellij.openapi.diagnostic.Logger 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.AgentService import com.tabbyml.intellijtabby.agent.AgentService
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -89,7 +88,7 @@ class CheckIssueDetail : AnAction() {
override fun update(e: AnActionEvent) { override fun update(e: AnActionEvent) {
val agentService = service<AgentService>() 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 { 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.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.components.service import com.intellij.openapi.components.service
import com.tabbyml.intellijtabby.editor.CompletionScheduler import com.tabbyml.intellijtabby.editor.CompletionProvider
class TriggerCompletion : AnAction() { class TriggerCompletion : AnAction() {
override fun actionPerformed(e: AnActionEvent) { override fun actionPerformed(e: AnActionEvent) {
val completionScheduler = service<CompletionScheduler>() val completionScheduler = service<CompletionProvider>()
val editor = e.getRequiredData(CommonDataKeys.EDITOR) val editor = e.getRequiredData(CommonDataKeys.EDITOR)
val offset = editor.caretModel.primaryCaret.offset val offset = editor.caretModel.primaryCaret.offset
completionScheduler.schedule(editor, offset, manually = true) completionScheduler.provideCompletion(editor, offset, manually = true)
} }
override fun update(e: AnActionEvent) { override fun update(e: AnActionEvent) {

View File

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

View File

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

View File

@ -5,44 +5,41 @@ import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.tabbyml.intellijtabby.agent.AgentService import com.tabbyml.intellijtabby.agent.AgentService
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Service @Service
class CompletionScheduler { class CompletionProvider {
private val logger = Logger.getInstance(CompletionScheduler::class.java) private val logger = Logger.getInstance(CompletionProvider::class.java)
data class CompletionContext(val editor: Editor, val offset: Int, val job: Job) data class CompletionContext(val editor: Editor, val offset: Int, val job: Job)
var scheduled: CompletionContext? = null private val ongoingCompletionFlow: MutableStateFlow<CompletionContext?> = MutableStateFlow(null)
private set 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 agentService = service<AgentService>()
val inlineCompletionService = service<InlineCompletionService>() val inlineCompletionService = service<InlineCompletionService>()
val settings = service<ApplicationSettingsState>()
clear() clear()
val job = agentService.scope.launch { val job = agentService.scope.launch {
if (!manually && !settings.isAutoCompletionEnabled) {
return@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 {
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
} }
} }
scheduled = CompletionContext(editor, offset, job) ongoingCompletionFlow.value = CompletionContext(editor, offset, job)
} }
fun clear() { fun clear() {
val inlineCompletionService = service<InlineCompletionService>() val inlineCompletionService = service<InlineCompletionService>()
inlineCompletionService.dismiss() inlineCompletionService.dismiss()
scheduled?.let { ongoingCompletionFlow.value?.let {
it.job.cancel() 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.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.util.messages.MessageBusConnection import com.intellij.util.messages.MessageBusConnection
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
class EditorListener : EditorFactoryListener { class EditorListener : EditorFactoryListener {
private val logger = Logger.getInstance(EditorListener::class.java) private val logger = Logger.getInstance(EditorListener::class.java)
@ -16,14 +17,17 @@ class EditorListener : EditorFactoryListener {
override fun editorCreated(event: EditorFactoryEvent) { override fun editorCreated(event: EditorFactoryEvent) {
val editor = event.editor val editor = event.editor
val editorManager = editor.project?.let { FileEditorManager.getInstance(it) } ?: return 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 { editor.caretModel.addCaretListener(object : CaretListener {
override fun caretPositionChanged(event: CaretEvent) { override fun caretPositionChanged(event: CaretEvent) {
if (editorManager.selectedTextEditor == editor) { if (editorManager.selectedTextEditor == editor) {
completionScheduler.scheduled?.let { completionProvider.ongoingCompletion.value.let {
if (it.editor != editor || it.offset != editor.caretModel.primaryCaret.offset) { if (it != null && it.editor == editor && it.offset == editor.caretModel.primaryCaret.offset) {
completionScheduler.clear() // keep ongoing completion
} else {
completionProvider.clear()
} }
} }
} }
@ -33,8 +37,10 @@ class EditorListener : EditorFactoryListener {
editor.document.addDocumentListener(object : DocumentListener { editor.document.addDocumentListener(object : DocumentListener {
override fun documentChanged(event: DocumentEvent) { override fun documentChanged(event: DocumentEvent) {
if (editorManager.selectedTextEditor == editor) { if (editorManager.selectedTextEditor == editor) {
val offset = event.offset + event.newFragment.length if (settings.completionTriggerMode == ApplicationSettingsState.TriggerMode.AUTOMATIC) {
completionScheduler.schedule(editor, offset) val offset = event.offset + event.newFragment.length
completionProvider.provideCompletion(editor, offset)
}
} }
} }
}) })
@ -42,10 +48,10 @@ class EditorListener : EditorFactoryListener {
editor.project?.messageBus?.connect()?.let { editor.project?.messageBus?.connect()?.let {
it.subscribe( it.subscribe(
FileEditorManagerListener.FILE_EDITOR_MANAGER, FileEditorManagerListener.FILE_EDITOR_MANAGER,
object: FileEditorManagerListener { object : FileEditorManagerListener {
override fun selectionChanged(event: FileEditorManagerEvent) { override fun selectionChanged(event: FileEditorManagerEvent) {
logger.info("FileEditorManagerListener selectionChanged.") logger.info("FileEditorManagerListener selectionChanged.")
completionScheduler.clear() completionProvider.clear()
} }
} }
) )
@ -54,9 +60,6 @@ class EditorListener : EditorFactoryListener {
} }
override fun editorReleased(event: EditorFactoryEvent) { override fun editorReleased(event: EditorFactoryEvent) {
messagesConnection[event.editor]?.let { messagesConnection[event.editor]?.disconnect()
it.disconnect()
it.dispose()
}
} }
} }

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package com.tabbyml.intellijtabby.settings package com.tabbyml.intellijtabby.settings
import com.google.gson.annotations.SerializedName
import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.Service import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State import com.intellij.openapi.components.State
@ -14,7 +15,14 @@ import kotlinx.coroutines.flow.asStateFlow
storages = [Storage("intellij-tabby.xml")] storages = [Storage("intellij-tabby.xml")]
) )
class ApplicationSettingsState : PersistentStateComponent<ApplicationSettingsState> { class ApplicationSettingsState : PersistentStateComponent<ApplicationSettingsState> {
var isAutoCompletionEnabled: Boolean = true enum class TriggerMode {
@SerializedName("manual")
MANUAL,
@SerializedName("automatic")
AUTOMATIC,
}
var completionTriggerMode: TriggerMode = TriggerMode.AUTOMATIC
set(value) { set(value) {
field = value field = value
stateFlow.value = this.data stateFlow.value = this.data
@ -31,14 +39,14 @@ class ApplicationSettingsState : PersistentStateComponent<ApplicationSettingsSta
} }
data class State( data class State(
val isAutoCompletionEnabled: Boolean, val completionTriggerMode: TriggerMode,
val serverEndpoint: String, val serverEndpoint: String,
val isAnonymousUsageTrackingDisabled: Boolean, val isAnonymousUsageTrackingDisabled: Boolean,
) )
val data: State val data: State
get() = State( get() = State(
isAutoCompletionEnabled = isAutoCompletionEnabled, completionTriggerMode = completionTriggerMode,
serverEndpoint = serverEndpoint, serverEndpoint = serverEndpoint,
isAnonymousUsageTrackingDisabled = isAnonymousUsageTrackingDisabled, 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.StatusBarWidget
import com.intellij.openapi.wm.impl.status.EditorBasedStatusBarPopup import com.intellij.openapi.wm.impl.status.EditorBasedStatusBarPopup
import com.intellij.openapi.wm.impl.status.widget.StatusBarEditorBasedWidgetFactory 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.Agent
import com.tabbyml.intellijtabby.agent.AgentService import com.tabbyml.intellijtabby.agent.AgentService
import com.tabbyml.intellijtabby.editor.CompletionProvider
import com.tabbyml.intellijtabby.settings.ApplicationSettingsState import com.tabbyml.intellijtabby.settings.ApplicationSettingsState
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.swing.Icon
class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() { class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
override fun getId(): String { override fun getId(): String {
@ -30,20 +33,28 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
} }
override fun createWidget(project: Project): StatusBarWidget { 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) { return object : EditorBasedStatusBarPopup(project, false) {
val updateStatusScope: CoroutineScope = CoroutineScope(Dispatchers.Main) val updateStatusScope: CoroutineScope = CoroutineScope(Dispatchers.Main)
val text = "Tabby" val text = "Tabby"
var icon = AllIcons.Actions.Refresh var icon: Icon = AnimatedIcon.Default()
var tooltip = "Tabby: Initializing" var tooltip = "Tabby: Initializing"
init { init {
val settings = service<ApplicationSettingsState>() val settings = service<ApplicationSettingsState>()
val agentService = service<AgentService>() val agentService = service<AgentService>()
val completionProvider = service<CompletionProvider>()
updateStatusScope.launch { updateStatusScope.launch {
combine(settings.state, agentService.status, agentService.currentIssue) { settings, agentStatus, currentIssue -> combine(settings.state, agentService.status, agentService.currentIssue, completionProvider.ongoingCompletion) { settings, agentStatus, currentIssue, ongoingCompletion ->
Triple(settings, agentStatus, currentIssue) CombinedState(settings, agentStatus, currentIssue, ongoingCompletion)
}.collect { }.collect {
updateStatus(it.first, it.second, it.third) updateStatus(it)
} }
} }
} }
@ -74,7 +85,7 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
return arrayOf( return arrayOf(
actionManager.getAction("Tabby.OpenAuthPage"), actionManager.getAction("Tabby.OpenAuthPage"),
actionManager.getAction("Tabby.CheckIssueDetail"), actionManager.getAction("Tabby.CheckIssueDetail"),
actionManager.getAction("Tabby.ToggleAutoCompletionEnabled"), actionManager.getAction("Tabby.ToggleInlineCompletionTriggerMode"),
actionManager.getAction("Tabby.OpenSettings"), actionManager.getAction("Tabby.OpenSettings"),
) )
} }
@ -86,41 +97,51 @@ class StatusBarWidgetFactory : StatusBarEditorBasedWidgetFactory() {
) )
} }
private fun updateStatus(settingsState: ApplicationSettingsState.State, agentStatus: Enum<*>, currentIssue: String?) { private fun updateStatus(state: CombinedState) {
if (!settingsState.isAutoCompletionEnabled) { when(state.agentStatus) {
icon = AllIcons.Windows.CloseSmall AgentService.Status.INITIALIZING, Agent.Status.NOT_INITIALIZED -> {
tooltip = "Tabby: Auto completion is disabled" icon = AnimatedIcon.Default()
} else { tooltip = "Tabby: Initializing"
when(agentStatus) { }
AgentService.Status.INITIALIZING, Agent.Status.NOT_INITIALIZED -> { AgentService.Status.INITIALIZATION_FAILED -> {
icon = AllIcons.Actions.Refresh icon = AllIcons.General.Error
tooltip = "Tabby: Initializing" tooltip = "Tabby: Initialization failed"
} }
AgentService.Status.INITIALIZATION_FAILED -> { Agent.Status.READY -> {
icon = AllIcons.General.Error if (state.currentIssue != null) {
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 -> {
icon = AllIcons.General.Warning icon = AllIcons.General.Warning
tooltip = "Tabby: Requires authorization" tooltip = when(state.currentIssue) {
}
Agent.Status.ISSUES_EXIST -> {
icon = AllIcons.General.Warning
tooltip = when(currentIssue) {
"slowCompletionResponseTime" -> "Tabby: Completion requests appear to take too much time" "slowCompletionResponseTime" -> "Tabby: Completion requests appear to take too much time"
"highCompletionTimeoutRate" -> "Tabby: Most completion requests timed out" "highCompletionTimeoutRate" -> "Tabby: Most completion requests timed out"
else -> "Tabby: Issues exist" 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 { invokeLater {
update { myStatusBar?.updateWidget(ID()) } update { myStatusBar?.updateWidget(ID()) }

View File

@ -22,17 +22,7 @@
<h2 id="demo">Demo</h2> <h2 id="demo">Demo</h2>
<p>Try our online demo <a href="https://tabby.tabbyml.com/playground/">here</a>.</p> <p>Try our online demo <a href="https://tabby.tabbyml.com/playground/">here</a>.</p>
<h2 id="requirements">Requirements</h2> <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> Tabby plugin requires <a href="https://nodejs.org/">Node.js</a> v18+ installed and added into <code>PATH</code> environment 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>
]]></description> ]]></description>
<!-- Product and plugin compatibility requirements. <!-- Product and plugin compatibility requirements.
@ -93,8 +83,8 @@
text="Check Issue Detail..." text="Check Issue Detail..."
description="Show detail information for current issue."> description="Show detail information for current issue.">
</action> </action>
<action id="Tabby.ToggleAutoCompletionEnabled" <action id="Tabby.ToggleInlineCompletionTriggerMode"
class="com.tabbyml.intellijtabby.actions.ToggleAutoCompletionEnabled"> class="com.tabbyml.intellijtabby.actions.ToggleInlineCompletionTriggerMode">
</action> </action>
<action id="Tabby.OpenSettings" <action id="Tabby.OpenSettings"
class="com.tabbyml.intellijtabby.actions.OpenSettings" class="com.tabbyml.intellijtabby.actions.OpenSettings"

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 107 KiB