diff --git a/.changeset/stale-towns-bathe.md b/.changeset/stale-towns-bathe.md new file mode 100644 index 00000000000..ac7605fa338 --- /dev/null +++ b/.changeset/stale-towns-bathe.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Jetbrains - Autocomplete Telemetry diff --git a/jetbrains/plugin/build.gradle.kts b/jetbrains/plugin/build.gradle.kts index a87d76d4484..d0de54f4081 100644 --- a/jetbrains/plugin/build.gradle.kts +++ b/jetbrains/plugin/build.gradle.kts @@ -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() @@ -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/") @@ -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() @@ -243,11 +244,11 @@ tasks { } } } - + val vscodePluginDir = File("./plugins/${ext.get("vscodePlugin")}") val depfile = File("prodDep.txt") val list = mutableListOf() - + // Read dependencies during execution doFirst { depfile.readLines().forEach { line -> @@ -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/") } diff --git a/jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/inline/InlineCompletionConstants.kt b/jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/inline/InlineCompletionConstants.kt index ff4811d2bb3..ee587e002a7 100644 --- a/jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/inline/InlineCompletionConstants.kt +++ b/jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/inline/InlineCompletionConstants.kt @@ -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 -} \ No newline at end of file +} diff --git a/jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/inline/KiloCodeInlineCompletionInsertHandler.kt b/jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/inline/KiloCodeInlineCompletionInsertHandler.kt new file mode 100644 index 00000000000..df25e6654f9 --- /dev/null +++ b/jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/inline/KiloCodeInlineCompletionInsertHandler.kt @@ -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, + ) { + // 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) + } +} diff --git a/jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/inline/KiloCodeInlineCompletionProvider.kt b/jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/inline/KiloCodeInlineCompletionProvider.kt index 5a2deddfae4..1e04e5b048a 100644 --- a/jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/inline/KiloCodeInlineCompletionProvider.kt +++ b/jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/inline/KiloCodeInlineCompletionProvider.kt @@ -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 @@ -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. @@ -47,37 +52,37 @@ class KiloCodeInlineCompletionProvider( val positionInfo = ReadAction.compute { 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 -> { @@ -104,14 +109,15 @@ 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. @@ -119,7 +125,7 @@ class KiloCodeInlineCompletionProvider( override fun isEnabled(event: InlineCompletionEvent): Boolean { return true } - + /** * Data class to hold position information calculated in read action */ @@ -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, ) -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6221eec745b..c724d31f1a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4973,7 +4973,6 @@ packages: '@lancedb/lancedb@0.21.3': resolution: {integrity: sha512-hfzp498BfcCJ730fV1YGGoXVxRgE+W1n0D0KwanKlbt8bBPSQ6E6Tf8mPXc8rKdAXIRR3o5mTzMG3z3Fda+m3Q==} engines: {node: '>= 18'} - cpu: [x64, arm64] os: [darwin, linux, win32] peerDependencies: apache-arrow: '>=15.0.0 <=18.1.0' @@ -27998,7 +27997,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.44.1)(tsx@4.19.4)(yaml@2.8.1) '@vitest/utils@2.0.5': dependencies: diff --git a/src/services/ghost/GhostServiceManager.ts b/src/services/ghost/GhostServiceManager.ts index 3aa547399d8..decb07d20ef 100644 --- a/src/services/ghost/GhostServiceManager.ts +++ b/src/services/ghost/GhostServiceManager.ts @@ -49,14 +49,13 @@ export class GhostServiceManager { // Register the providers this.codeActionProvider = new GhostCodeActionProvider() - const { kiloCodeWrapperJetbrains } = getKiloCodeWrapperProperties() this.inlineCompletionProvider = new GhostInlineCompletionProvider( this.context, this.model, this.updateCostTracking.bind(this), () => this.settings, this.cline, - !kiloCodeWrapperJetbrains ? new AutocompleteTelemetry() : null, + new AutocompleteTelemetry(), ) void this.load()