Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/stale-towns-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Jetbrains - Autocomplete Telemetry
45 changes: 23 additions & 22 deletions jetbrains/plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ project.afterEvaluate {
tasks.findByName(":prepareSandbox")?.inputs?.properties?.put("build_mode", ext.get("debugMode"))
}


group = properties("pluginGroup").get()
version = properties("pluginVersion").get()

Expand Down Expand Up @@ -152,60 +151,62 @@ tasks {
println("Configuration file generated: ${configFile.absolutePath}")
}
}


buildPlugin {
dependsOn(prepareSandbox)

// Include the jetbrains directory contents from sandbox in the distribution root
doLast {
if (ext.get("debugMode") != "idea" && ext.get("debugMode") != "none") {
val distributionFile = archiveFile.get().asFile
val sandboxPluginsDir = layout.buildDirectory.get().asFile.resolve("idea-sandbox/IC-2024.3/plugins")
val jetbrainsDir = sandboxPluginsDir.resolve("jetbrains")

if (jetbrainsDir.exists() && distributionFile.exists()) {
logger.lifecycle("Adding sandbox resources to distribution ZIP...")
logger.lifecycle("Sandbox jetbrains dir: ${jetbrainsDir.absolutePath}")
logger.lifecycle("Distribution file: ${distributionFile.absolutePath}")

// Extract the existing ZIP
val tempDir = layout.buildDirectory.get().asFile.resolve("temp-dist")
tempDir.deleteRecursively()
tempDir.mkdirs()

copy {
from(zipTree(distributionFile))
into(tempDir)
}

// Copy jetbrains directory CONTENTS directly to plugin root (not the jetbrains folder itself)
val pluginDir = tempDir.resolve(rootProject.name)
copy {
from(jetbrainsDir) // Copy contents of jetbrains dir
into(pluginDir) // Directly into plugin root
into(pluginDir) // Directly into plugin root
}

// Re-create the ZIP with resources included
distributionFile.delete()
ant.invokeMethod("zip", mapOf(
"destfile" to distributionFile.absolutePath,
"basedir" to tempDir.absolutePath
))

ant.invokeMethod(
"zip",
mapOf(
"destfile" to distributionFile.absolutePath,
"basedir" to tempDir.absolutePath,
),
)

// Clean up temp directory
tempDir.deleteRecursively()

logger.lifecycle("Distribution ZIP updated with sandbox resources at root level")
}
}
}
}

prepareSandbox {
dependsOn("generateConfigProperties")
duplicatesStrategy = DuplicatesStrategy.INCLUDE

if (ext.get("debugMode") == "idea") {
from("${project.projectDir.absolutePath}/src/main/resources/themes/") {
into("${ext.get("debugResource")}/${ext.get("vscodePlugin")}/integrations/theme/default-themes/")
Expand All @@ -226,14 +227,14 @@ tasks {
if (!depfile.exists()) {
throw IllegalStateException("missing prodDep.txt")
}

// Handle platform.zip for release mode
if (ext.get("debugMode") == "release") {
val platformZip = File("platform.zip")
if (!platformZip.exists() || platformZip.length() < 1024 * 1024) {
throw IllegalStateException("platform.zip file does not exist or is smaller than 1MB. This file is supported through git lfs and needs to be obtained through git lfs")
}

// Extract platform.zip to the platform subdirectory under the project build directory
val platformDir = File("${layout.buildDirectory.get().asFile}/platform")
platformDir.mkdirs()
Expand All @@ -243,11 +244,11 @@ tasks {
}
}
}

val vscodePluginDir = File("./plugins/${ext.get("vscodePlugin")}")
val depfile = File("prodDep.txt")
val list = mutableListOf<String>()

// Read dependencies during execution
doFirst {
depfile.readLines().forEach { line ->
Expand All @@ -273,7 +274,7 @@ tasks {

// Copy VSCode plugin extension
from("${vscodePluginDir.path}/extension") { into("$pluginName/${ext.get("vscodePlugin")}") }

// Copy themes
from("src/main/resources/themes/") { into("$pluginName/${ext.get("vscodePlugin")}/integrations/theme/default-themes/") }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@ object InlineCompletionConstants {
* VSCode extension command ID for inline completion generation.
*/
const val EXTERNAL_COMMAND_ID = "kilo-code.jetbrains.getInlineCompletions"


/**
* Command ID registered in the VSCode extension for tracking acceptance events.
* This matches the command registered in GhostInlineCompletionProvider.
*/
const val INLINE_COMPLETION_ACCEPTED_COMMAND = "kilocode.ghost.inline-completion.accepted"

/**
* Default timeout in milliseconds for inline completion requests.
* Set to 10 seconds to allow sufficient time for LLM response.
*/
const val RPC_TIMEOUT_MS = 10000L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
//
// SPDX-License-Identifier: Apache-2.0

package ai.kilocode.jetbrains.inline

import ai.kilocode.jetbrains.core.PluginContext
import ai.kilocode.jetbrains.core.ServiceProxyRegistry
import ai.kilocode.jetbrains.ipc.proxy.interfaces.ExtHostCommandsProxy
import com.intellij.codeInsight.inline.completion.DefaultInlineCompletionInsertHandler
import com.intellij.codeInsight.inline.completion.InlineCompletionInsertEnvironment
import com.intellij.codeInsight.inline.completion.elements.InlineCompletionElement
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project

/**
* Custom insert handler that triggers telemetry when inline completions are accepted.
* Extends DefaultInlineCompletionInsertHandler to maintain default insertion behavior
* while adding telemetry tracking via RPC to the VSCode extension.
*/
class KiloCodeInlineCompletionInsertHandler(
private val project: Project,
) : DefaultInlineCompletionInsertHandler() {

private val logger = Logger.getInstance(KiloCodeInlineCompletionInsertHandler::class.java)

/**
* Called after the completion text has been inserted into the document.
* This is our hook to trigger telemetry tracking.
*
* @param environment Contains information about the insertion context
* @param elements The inline completion elements that were inserted
*/
override fun afterInsertion(
environment: InlineCompletionInsertEnvironment,
elements: List<InlineCompletionElement>,
) {
// Note: NOT calling super.afterInsertion() to avoid potential duplicate telemetry
// The default implementation may be empty or may trigger its own telemetry

// Trigger telemetry via RPC
try {
val proxy = getRPCProxy()
if (proxy != null) {
// Execute the acceptance command asynchronously
// No need to wait for the result as this is fire-and-forget telemetry
proxy.executeContributedCommand(
InlineCompletionConstants.INLINE_COMPLETION_ACCEPTED_COMMAND,
emptyList(),
)
logger.debug("Triggered inline completion acceptance telemetry")
} else {
logger.warn("Failed to trigger acceptance telemetry - RPC proxy not available")
}
} catch (e: Exception) {
// Don't let telemetry errors affect the user experience
logger.warn("Error triggering acceptance telemetry", e)
}
}

/**
* Gets the RPC proxy for command execution from the project's PluginContext.
*/
private fun getRPCProxy(): ExtHostCommandsProxy? {
return project.getService(PluginContext::class.java)
?.getRPCProtocol()
?.getProxy(ServiceProxyRegistry.ExtHostContext.ExtHostCommands)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.intellij.codeInsight.inline.completion.InlineCompletionProviderID
import com.intellij.codeInsight.inline.completion.InlineCompletionRequest
import com.intellij.codeInsight.inline.completion.elements.InlineCompletionGrayTextElement
import com.intellij.codeInsight.inline.completion.suggestion.InlineCompletionSingleSuggestion
import com.intellij.codeInsight.inline.completion.suggestion.InlineCompletionSuggestion
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.fileEditor.FileDocumentManager
Expand All @@ -24,18 +23,24 @@ class KiloCodeInlineCompletionProvider(
private val handle: Int,
private val project: Project,
private val extensionId: String,
private val displayName: String?
private val displayName: String?,
) : InlineCompletionProvider {

private val logger = Logger.getInstance(KiloCodeInlineCompletionProvider::class.java)
private val completionService = InlineCompletionService.getInstance()


/**
* Custom insert handler that triggers telemetry when completions are accepted.
* Overrides the default insertHandler from InlineCompletionProvider.
*/
override val insertHandler = KiloCodeInlineCompletionInsertHandler(project)

/**
* Unique identifier for this provider.
* Required by InlineCompletionProvider interface.
*/
override val id: InlineCompletionProviderID = InlineCompletionProviderID("kilocode-inline-completion-$extensionId-$handle")

/**
* Gets inline completion suggestions using the Ghost service.
* Sends full file content to ensure accurate completions.
Expand All @@ -47,37 +52,37 @@ class KiloCodeInlineCompletionProvider(
val positionInfo = ReadAction.compute<PositionInfo, Throwable> {
val editor = request.editor
val document = editor.document

// Use request.endOffset which is the correct insertion point for the completion
// This is where IntelliJ expects the completion to be inserted
val completionOffset = request.endOffset

// Calculate line and character position from the completion offset
val line = document.getLineNumber(completionOffset)
val lineStartOffset = document.getLineStartOffset(line)
val char = completionOffset - lineStartOffset

// Get language ID from file type
val virtualFile = FileDocumentManager.getInstance().getFile(document)
val langId = virtualFile?.fileType?.name?.lowercase() ?: "text"

// Also get caret position for logging/debugging
val caretOffset = editor.caretModel.offset

PositionInfo(completionOffset, line, char, langId, document, caretOffset)
}

val (offset, lineNumber, character, languageId, document, caretOffset) = positionInfo

// Call the new service with full file content
val result = completionService.getInlineCompletions(
project,
document,
lineNumber,
character,
languageId
languageId,
)

// Convert result to InlineCompletionSingleSuggestion using the new API
return when (result) {
is InlineCompletionService.Result.Success -> {
Expand All @@ -104,22 +109,23 @@ class KiloCodeInlineCompletionProvider(
} catch (e: Exception) {
// Check if this is a wrapped cancellation
if (e.cause is kotlinx.coroutines.CancellationException ||
e.cause is java.util.concurrent.CancellationException) {
e.cause is java.util.concurrent.CancellationException
) {
return InlineCompletionSingleSuggestion.build { }
}
// Real error - log appropriately
return InlineCompletionSingleSuggestion.build { }
}
}

/**
* Determines if this provider is enabled for the given event.
* Document selector matching is handled during registration.
*/
override fun isEnabled(event: InlineCompletionEvent): Boolean {
return true
}

/**
* Data class to hold position information calculated in read action
*/
Expand All @@ -129,6 +135,6 @@ class KiloCodeInlineCompletionProvider(
val character: Int,
val languageId: String,
val document: com.intellij.openapi.editor.Document,
val caretOffset: Int
val caretOffset: Int,
)
}
}
3 changes: 1 addition & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading