diff --git a/.changeset/large-jars-train.md b/.changeset/large-jars-train.md new file mode 100644 index 00000000000..0343c1a39cc --- /dev/null +++ b/.changeset/large-jars-train.md @@ -0,0 +1,9 @@ +--- +"kilo-code": patch +--- + +chore: update Gemini Cli models and metadata + +- Added gemini-3-flash-preview model configuration. +- Updated maxThinkingTokens for gemini-3-pro-preview to 32,768. +- Reordered model definitions to prioritize newer versions. diff --git a/.changeset/small-towns-march.md b/.changeset/small-towns-march.md new file mode 100644 index 00000000000..8be9da81341 --- /dev/null +++ b/.changeset/small-towns-march.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Add chat autocomplete telemetry 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/.changeset/yellow-plants-work.md b/.changeset/yellow-plants-work.md new file mode 100644 index 00000000000..9bb67bcd17f --- /dev/null +++ b/.changeset/yellow-plants-work.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +fix(ollama): fix model not found error and context window display diff --git a/apps/kilocode-docs/docs/advanced-usage/slackbot.md b/apps/kilocode-docs/docs/advanced-usage/slackbot.md new file mode 100644 index 00000000000..3c9bf22f1dc --- /dev/null +++ b/apps/kilocode-docs/docs/advanced-usage/slackbot.md @@ -0,0 +1,147 @@ +--- +title: Kilo Slackbot +sidebar_label: Kilo Slackbot +--- + +# Kilo Slackbot + +The Kilo Slackbot brings the power of Kilo Code directly into your Slack workspace. Ask questions about your repositories, request code implementations, or get help with issues—all without leaving Slack. + +--- + +## What You Can Do With the Slackbot + +- **Ask questions about your repositories** — Get explanations about code, architecture, or implementation details +- **Request code implementations** — Tell the bot to implement fixes or features suggested in Slack threads +- **Get help with debugging** — Share error messages or issues and get AI-powered assistance +- **Collaborate with your team** — Mention the bot in any channel to get help in context + +--- + +## Prerequisites + +Before using the Kilo Slackbot: + +- You must have a **Kilo Code account** with available credits +- Your **GitHub Integration must be configured** via the [Integrations tab](https://app.kilo.ai/integrations) so the Slackbot can access your repositories + +To install the Kilo Slackbot, simply go to the integrations menu in the sidebar on https://app.kilo.ai and set up the Slack integration. + +--- + +## How to Interact with the Slackbot + +### Direct Messages + +You can message the Kilo Slackbot directly through Slack DMs for private conversations: + +1. Find **Kilo** in your Slack workspace's app list +2. Start a direct message conversation +3. Ask your question or describe what you need + +This is ideal for: +- Private questions about your code +- Sensitive debugging sessions +- Personal productivity tasks + +### Channel Mentions + +Mention the bot in any channel where it's been added: + +``` +@Kilo can you explain how the authentication flow works in our backend? +``` + +This is great for: +- Team discussions where AI assistance would help +- Collaborative debugging sessions +- Getting quick answers during code reviews + +--- + +## Use Cases + +### Ask Questions About Your Repositories + +Get instant answers about your codebase without switching contexts: + +``` +@Kilo what does the UserService class do in our main backend repo? +``` + +``` +@Kilo how is error handling implemented in the payment processing module? +``` + +### Implement Fixes from Slack Discussions + +When your team identifies an issue or improvement in a Slack thread, ask the bot to implement it: + +``` +@Kilo based on this thread, can you implement the fix for the null pointer exception in the order processing service? +``` + +The bot can: +- Read the context from the thread +- Understand the proposed solution +- Create a branch with the implementation +- Push the changes to your repository + +### Debug Issues + +Share error messages or stack traces and get help: + +``` +@Kilo I'm seeing this error in production: +[paste error message] +Can you help me understand what's causing it? +``` + +--- + +## How It Works + +1. **Message the bot** — Either through DMs or by mentioning it in a channel +2. **Bot processes your request** — The Slackbot uses your connected GitHub repositories to understand context +3. **AI generates a response** — Kilo Code's AI analyzes your request and provides helpful responses +4. **Code changes (if requested)** — For implementation requests, the bot can create pull requests + +--- + +## Cost + +- **Kilo Code credits are used** when the Slackbot performs work (model usage, operations, etc.) +- Credit usage is similar to using Kilo Code through other interfaces + +--- + +## Tips for Best Results + +- **Be specific** — The more context you provide, the better the response +- **Reference specific files or functions** — Help the bot understand exactly what you're asking about +- **Use threads** — Keep related conversations in threads for better context +- **Specify the repository** — If you have multiple repos connected, mention which one you're asking about + +--- + +## Limitations + +- The Slackbot can only access repositories you've connected through the [Integrations](https://app.kilo.ai/integrations) page +- Complex multi-step implementations may require follow-up messages +- Response times may vary based on the complexity of your request + +--- + +## Troubleshooting + +**"The bot isn't responding."** +Ensure the Kilo Slackbot is installed in your workspace and has been added to the channel you're using. + +**"The bot can't access my repository."** +Verify your GitHub integration is configured correctly in the [Integrations tab](https://app.kilo.ai/integrations). + +**"I'm getting incomplete responses."** +Try breaking your request into smaller, more specific questions. + +**"The bot doesn't understand my codebase."** +Make sure the repository you're asking about is connected and accessible through your GitHub integration. diff --git a/apps/kilocode-docs/sidebars.ts b/apps/kilocode-docs/sidebars.ts index 0e3ae210ca8..1b40ad47756 100644 --- a/apps/kilocode-docs/sidebars.ts +++ b/apps/kilocode-docs/sidebars.ts @@ -157,6 +157,7 @@ const sidebars: SidebarsConfig = { "features/auto-launch-configuration", "advanced-usage/auto-cleanup", "advanced-usage/integrations", + "advanced-usage/slackbot", "advanced-usage/appbuilder", "advanced-usage/cloud-agent", "advanced-usage/code-reviews", 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/packages/types/src/providers/gemini-cli.ts b/packages/types/src/providers/gemini-cli.ts index 6ec6ccc07c5..73951e2ee36 100644 --- a/packages/types/src/providers/gemini-cli.ts +++ b/packages/types/src/providers/gemini-cli.ts @@ -7,15 +7,25 @@ export type GeminiCliModelId = keyof typeof geminiCliModels export const geminiCliDefaultModelId: GeminiCliModelId = "gemini-2.5-flash" export const geminiCliModels = { - "gemini-2.5-flash": { + "gemini-3-pro-preview": { + maxTokens: 64_000, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + supportsReasoningBudget: true, + maxThinkingTokens: 32_768, + }, + "gemini-3-flash-preview": { maxTokens: 64_000, contextWindow: 1_048_576, supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, - maxThinkingTokens: 24_576, supportsReasoningBudget: true, + maxThinkingTokens: 32_768, }, "gemini-2.5-pro": { maxTokens: 64_000, @@ -28,14 +38,14 @@ export const geminiCliModels = { supportsReasoningBudget: true, requiredReasoningBudget: true, }, - "gemini-3-pro-preview": { + "gemini-2.5-flash": { maxTokens: 64_000, contextWindow: 1_048_576, supportsImages: true, supportsPromptCache: false, inputPrice: 0, outputPrice: 0, + maxThinkingTokens: 24_576, supportsReasoningBudget: true, - maxThinkingTokens: 64_000, }, } as const satisfies Record 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/api/providers/fetchers/ollama.ts b/src/api/providers/fetchers/ollama.ts index 8581b71d7b7..49fecd940fc 100644 --- a/src/api/providers/fetchers/ollama.ts +++ b/src/api/providers/fetchers/ollama.ts @@ -127,7 +127,7 @@ export async function getOllamaModels( } else { console.error(`Error parsing Ollama models response: ${JSON.stringify(parsedResponse.error, null, 2)}`) } - } catch (error) { + } catch (error: any) { if (error.code === "ECONNREFUSED") { console.warn(`Failed connecting to Ollama at ${baseUrl}`) } else { diff --git a/src/api/providers/native-ollama.ts b/src/api/providers/native-ollama.ts index 9eac15e9438..4f4d3ef0620 100644 --- a/src/api/providers/native-ollama.ts +++ b/src/api/providers/native-ollama.ts @@ -1,7 +1,12 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" import { Message, Ollama, Tool as OllamaTool, type Config as OllamaOptions } from "ollama" -import { ModelInfo, openAiModelInfoSaneDefaults, DEEP_SEEK_DEFAULT_TEMPERATURE } from "@roo-code/types" +import { + ModelInfo, + openAiModelInfoSaneDefaults, + DEEP_SEEK_DEFAULT_TEMPERATURE, + ollamaDefaultModelInfo, +} from "@roo-code/types" import { ApiStream } from "../transform/stream" import { BaseProvider } from "./base-provider" import type { ApiHandlerOptions } from "../../shared/api" @@ -162,11 +167,13 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio private client: Ollama | undefined protected models: Record = {} private isInitialized = false // kilocode_change + private modelFetchError: string | null = null // kilocode_change - track fetch errors for better diagnostics + private initializationPromise: Promise | null = null // kilocode_change - prevent race condition constructor(options: ApiHandlerOptions) { super() this.options = options - this.initialize() // kilocode_change + this.initializationPromise = this.initialize() // kilocode_change - store the promise } // kilocode_change start @@ -177,6 +184,12 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio await this.fetchModel() this.isInitialized = true } + + private async ensureInitialized(): Promise { + if (this.initializationPromise) { + await this.initializationPromise + } + } // kilocode_change end private ensureClient(): Ollama { @@ -231,9 +244,7 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio metadata?: ApiHandlerCreateMessageMetadata, ): ApiStream { // kilocode_change start - if (!this.isInitialized) { - await this.initialize() - } + await this.ensureInitialized() // kilocode_change end const client = this.ensureClient() @@ -377,42 +388,76 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio async fetchModel() { // kilocode_change start - this.models = await getOllamaModels( - this.options.ollamaBaseUrl, - this.options.ollamaApiKey, - this.options.ollamaNumCtx, - ) + try { + this.modelFetchError = null + this.models = await getOllamaModels( + this.options.ollamaBaseUrl, + this.options.ollamaApiKey, + this.options.ollamaNumCtx, + ) + if (Object.keys(this.models).length === 0) { + this.modelFetchError = "noModelsReturned" + } + } catch (error: any) { + this.modelFetchError = error.message || "unknownError" + this.models = {} + } return this.models // kilocode_change end } override getModel(): { id: string; info: ModelInfo } { const modelId = this.options.ollamaModelId || "" + const userContextWindow = this.options.ollamaNumCtx // kilocode_change start + // If not yet initialized, return default model info to allow getEnvironmentDetails to work + // The actual model validation will happen in createMessage() after ensureInitialized() + if (!this.isInitialized) { + const contextWindow = userContextWindow || ollamaDefaultModelInfo.contextWindow + return { + id: modelId, + info: { + ...ollamaDefaultModelInfo, + contextWindow: contextWindow, + }, + } + } + const modelInfo = this.models[modelId] if (!modelInfo) { const availableModels = Object.keys(this.models) - const errorMessage = - availableModels.length > 0 - ? `Model ${modelId} not found. Available models: ${availableModels.join(", ")}` - : `Model ${modelId} not found. No models available.` - throw new Error(errorMessage) + if (availableModels.length > 0) { + throw new Error(`Model ${modelId} not found. Available models: ${availableModels.join(", ")}`) + } + + const baseUrl = this.options.ollamaBaseUrl || "http://localhost:11434" + const troubleshooting = [ + `1. Ensure Ollama is running (try: ollama serve)`, + `2. Verify the base URL is correct: ${baseUrl}`, + `3. Check that models are installed (try: ollama list)`, + `4. Pull the model if needed: ollama pull ${modelId}`, + ].join("\n") + + throw new Error( + `Model ${modelId} not found. Could not retrieve models from Ollama.\n\nTroubleshooting:\n${troubleshooting}`, + ) } + + // Override contextWindow with user's setting if provided + const finalModelInfo = userContextWindow ? { ...modelInfo, contextWindow: userContextWindow } : modelInfo // kilocode_change end return { id: modelId, - info: modelInfo, // kilocode_change + info: finalModelInfo, // kilocode_change } } async completePrompt(prompt: string): Promise { try { // kilocode_change start - if (!this.isInitialized) { - await this.initialize() - } + await this.ensureInitialized() // kilocode_change end const client = this.ensureClient() diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index f2cf40bc916..99deac1e3cb 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -88,6 +88,8 @@ import { deviceAuthMessageHandler, } from "../kilocode/webview/webviewMessageHandlerUtils" import { GhostServiceManager } from "../../services/ghost/GhostServiceManager" +import { handleChatCompletionRequest } from "../../services/ghost/chat-autocomplete/handleChatCompletionRequest" +import { handleChatCompletionAccepted } from "../../services/ghost/chat-autocomplete/handleChatCompletionAccepted" // kilocode_change end const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"]) @@ -3707,9 +3709,6 @@ export const webviewMessageHandler = async ( break } // kilocode_change start: Chat text area FIM autocomplete case "requestChatCompletion": { - const { handleChatCompletionRequest } = await import( - "../../services/ghost/chat-autocomplete/handleChatCompletionRequest" - ) await handleChatCompletionRequest( message as WebviewMessage & { type: "requestChatCompletion" }, provider, @@ -3717,6 +3716,10 @@ export const webviewMessageHandler = async ( ) break } + case "chatCompletionAccepted": { + handleChatCompletionAccepted(message as WebviewMessage & { type: "chatCompletionAccepted" }) + break + } // kilocode_change end: Chat text area FIM autocomplete case "openCommandFile": { try { diff --git a/src/services/code-index/managed/ignore-list.ts b/src/services/code-index/managed/ignore-list.ts index 0158ab476c5..6eac68497b1 100644 --- a/src/services/code-index/managed/ignore-list.ts +++ b/src/services/code-index/managed/ignore-list.ts @@ -41,7 +41,8 @@ export function shouldIgnoreFile(relativeFilePath: string): boolean { ext === ".min.js" || ext === ".min.css" || ext === ".bundle.js" || - ext === ".map" + ext === ".map" || + fileName.match(/\.dist\..+$/) ) { return true } 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() diff --git a/src/services/ghost/chat-autocomplete/ChatTextAreaAutocomplete.ts b/src/services/ghost/chat-autocomplete/ChatTextAreaAutocomplete.ts index 3d7ad42d540..cc153a5733d 100644 --- a/src/services/ghost/chat-autocomplete/ChatTextAreaAutocomplete.ts +++ b/src/services/ghost/chat-autocomplete/ChatTextAreaAutocomplete.ts @@ -1,9 +1,10 @@ import * as vscode from "vscode" import { GhostModel } from "../GhostModel" import { ProviderSettingsManager } from "../../../core/config/ProviderSettingsManager" -import { VisibleCodeContext } from "../types" +import { AutocompleteContext, VisibleCodeContext } from "../types" import { ApiStreamChunk } from "../../../api/transform/stream" import { removePrefixOverlap } from "../../continuedev/core/autocomplete/postprocessing/removePrefixOverlap.js" +import { AutocompleteTelemetry } from "../classic-auto-complete/AutocompleteTelemetry" /** * Service for providing FIM-based autocomplete suggestions in ChatTextArea @@ -11,10 +12,12 @@ import { removePrefixOverlap } from "../../continuedev/core/autocomplete/postpro export class ChatTextAreaAutocomplete { private model: GhostModel private providerSettingsManager: ProviderSettingsManager + private telemetry: AutocompleteTelemetry constructor(providerSettingsManager: ProviderSettingsManager) { this.model = new GhostModel() this.providerSettingsManager = providerSettingsManager + this.telemetry = new AutocompleteTelemetry("chat-textarea") } async initialize(): Promise { @@ -30,6 +33,18 @@ export class ChatTextAreaAutocomplete { } async getCompletion(userText: string, visibleCodeContext?: VisibleCodeContext): Promise<{ suggestion: string }> { + const startTime = Date.now() + + // Build context for telemetry + const context: AutocompleteContext = { + languageId: "chat", // Chat textarea doesn't have a language ID + modelId: this.model.getModelName(), + provider: this.model.getProviderDisplayName(), + } + + // Capture suggestion requested + this.telemetry.captureSuggestionRequested(context) + if (!this.model.loaded) { const loaded = await this.initialize() if (!loaded) { @@ -47,29 +62,60 @@ export class ChatTextAreaAutocomplete { let response = "" - // Use FIM if supported, otherwise fall back to chat-based completion - if (this.model.supportsFim()) { - await this.model.generateFimResponse(prefix, suffix, (chunk) => { - response += chunk - }) - } else { - // Fall back to chat-based completion for models without FIM support - const systemPrompt = this.getChatSystemPrompt() - const userPrompt = this.getChatUserPrompt(prefix) - - await this.model.generateResponse(systemPrompt, userPrompt, (chunk) => { - if (chunk.type === "text") { - response += chunk.text - } - }) - } + try { + // Use FIM if supported, otherwise fall back to chat-based completion + if (this.model.supportsFim()) { + await this.model.generateFimResponse(prefix, suffix, (chunk) => { + response += chunk + }) + } else { + // Fall back to chat-based completion for models without FIM support + const systemPrompt = this.getChatSystemPrompt() + const userPrompt = this.getChatUserPrompt(prefix) + + await this.model.generateResponse(systemPrompt, userPrompt, (chunk) => { + if (chunk.type === "text") { + response += chunk.text + } + }) + } - const cleanedSuggestion = this.cleanSuggestion(response, userText) - console.log( - `[ChatAutocomplete] prefix: ${JSON.stringify(userText)} | response: ${JSON.stringify(response)} | cleanedSuggestion: ${JSON.stringify(cleanedSuggestion)}`, - ) + const latencyMs = Date.now() - startTime + + // Capture successful LLM request + this.telemetry.captureLlmRequestCompleted( + { + latencyMs, + // Token counts not available from current API + }, + context, + ) + + const cleanedSuggestion = this.cleanSuggestion(response, userText) + + // Track if suggestion was filtered or returned + if (!cleanedSuggestion) { + if (!response.trim()) { + this.telemetry.captureSuggestionFiltered("empty_response", context) + } else { + this.telemetry.captureSuggestionFiltered("filtered_by_postprocessing", context) + } + } else { + this.telemetry.captureLlmSuggestionReturned(context, cleanedSuggestion.length) + } - return { suggestion: cleanedSuggestion } + return { suggestion: cleanedSuggestion } + } catch (error) { + const latencyMs = Date.now() - startTime + this.telemetry.captureLlmRequestFailed( + { + latencyMs, + error: error instanceof Error ? error.message : String(error), + }, + context, + ) + return { suggestion: "" } + } } /** diff --git a/src/services/ghost/chat-autocomplete/handleChatCompletionAccepted.ts b/src/services/ghost/chat-autocomplete/handleChatCompletionAccepted.ts new file mode 100644 index 00000000000..ec00ba82aa7 --- /dev/null +++ b/src/services/ghost/chat-autocomplete/handleChatCompletionAccepted.ts @@ -0,0 +1,25 @@ +import { WebviewMessage } from "../../../shared/WebviewMessage" +import { AutocompleteTelemetry } from "../classic-auto-complete/AutocompleteTelemetry" + +// Singleton telemetry instance for chat-textarea autocomplete +// This ensures we use the same instance across requests and acceptance events +let telemetryInstance: AutocompleteTelemetry | null = null + +/** + * Get or create the telemetry instance for chat-textarea autocomplete + */ +export function getChatAutocompleteTelemetry(): AutocompleteTelemetry { + if (!telemetryInstance) { + telemetryInstance = new AutocompleteTelemetry("chat-textarea") + } + return telemetryInstance +} + +/** + * Handles a chat completion accepted event from the webview. + * Captures telemetry when the user accepts a suggestion via Tab or ArrowRight. + */ +export function handleChatCompletionAccepted(message: WebviewMessage & { type: "chatCompletionAccepted" }): void { + const telemetry = getChatAutocompleteTelemetry() + telemetry.captureAcceptSuggestion(message.suggestionLength) +} diff --git a/src/services/ghost/classic-auto-complete/AutocompleteTelemetry.ts b/src/services/ghost/classic-auto-complete/AutocompleteTelemetry.ts index 24a92f03692..604dd54e968 100644 --- a/src/services/ghost/classic-auto-complete/AutocompleteTelemetry.ts +++ b/src/services/ghost/classic-auto-complete/AutocompleteTelemetry.ts @@ -4,23 +4,38 @@ import type { AutocompleteContext, CacheMatchType } from "../types" export type { AutocompleteContext, CacheMatchType } +/** + * Type of autocomplete being used + * - "inline": Classic inline code completion in the editor + * - "chat-textarea": Autocomplete in the chat input textarea + */ +export type AutocompleteType = "inline" | "chat-textarea" + /** * Telemetry service for autocomplete events. * Can be initialized without parameters and injected into components that need telemetry tracking. + * Supports different autocomplete types via the `autocompleteType` property. */ export class AutocompleteTelemetry { - constructor() {} + private readonly autocompleteType: AutocompleteType + + /** + * Create a new AutocompleteTelemetry instance + * @param autocompleteType - The type of autocomplete (defaults to "inline" for backward compatibility) + */ + constructor(autocompleteType: AutocompleteType = "inline") { + this.autocompleteType = autocompleteType + } private captureEvent(event: TelemetryEventName, properties?: Record): void { // also log to console: if (TelemetryService.hasInstance()) { - if (properties !== undefined) { - TelemetryService.instance.captureEvent(event, properties) - console.log(`Autocomplete Telemetry event: ${event}`, properties) - } else { - TelemetryService.instance.captureEvent(event) - console.log(`Autocomplete Telemetry event: ${event}`) + const propsWithType = { + ...properties, + autocompleteType: this.autocompleteType, } + TelemetryService.instance.captureEvent(event, propsWithType) + console.log(`Autocomplete Telemetry event: ${event}`, propsWithType) } } @@ -98,9 +113,9 @@ export class AutocompleteTelemetry { public captureLlmRequestCompleted( properties: { latencyMs: number - cost: number - inputTokens: number - outputTokens: number + cost?: number + inputTokens?: number + outputTokens?: number }, context: AutocompleteContext, ): void { @@ -132,8 +147,12 @@ export class AutocompleteTelemetry { * There are two ways to analyze what percentage was accepted: * 1. Sum of this event divided by the sum of the suggestion returned event * 2. Sum of this event divided by the sum of the suggestion returned + cache hit events + * + * @param suggestionLength - Optional length of the accepted suggestion */ - public captureAcceptSuggestion(): void { - this.captureEvent(TelemetryEventName.AUTOCOMPLETE_ACCEPT_SUGGESTION) + public captureAcceptSuggestion(suggestionLength?: number): void { + this.captureEvent(TelemetryEventName.AUTOCOMPLETE_ACCEPT_SUGGESTION, { + ...(suggestionLength !== undefined && { suggestionLength }), + }) } } diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 07ff1ddbed4..5bfedc5cfd2 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -287,7 +287,9 @@ export interface WebviewMessage { | "cancelDeviceAuth" // kilocode_change: Cancel device auth flow | "deviceAuthCompleteWithProfile" // kilocode_change: Device auth complete with specific profile | "requestChatCompletion" // kilocode_change: Request FIM completion for chat text area + | "chatCompletionAccepted" // kilocode_change: User accepted a chat completion suggestion text?: string + suggestionLength?: number // kilocode_change: Length of accepted suggestion for telemetry completionRequestId?: string // kilocode_change shareId?: string // kilocode_change - for sessionFork sessionId?: string // kilocode_change - for sessionSelect diff --git a/webview-ui/src/components/chat/hooks/useChatGhostText.ts b/webview-ui/src/components/chat/hooks/useChatGhostText.ts index 72073f04571..a31824c53ba 100644 --- a/webview-ui/src/components/chat/hooks/useChatGhostText.ts +++ b/webview-ui/src/components/chat/hooks/useChatGhostText.ts @@ -65,6 +65,11 @@ export function useChatGhostText({ event.preventDefault() skipNextCompletionRef.current = true insertTextAtCursor(textArea, ghostText) + // Send telemetry event for accepted suggestion + vscode.postMessage({ + type: "chatCompletionAccepted", + suggestionLength: ghostText.length, + }) setGhostText("") return true } @@ -81,6 +86,11 @@ export function useChatGhostText({ skipNextCompletionRef.current = true const { word, remainder } = extractNextWord(ghostText) insertTextAtCursor(textArea, word) + // Send telemetry event for accepted word + vscode.postMessage({ + type: "chatCompletionAccepted", + suggestionLength: word.length, + }) setGhostText(remainder) return true } diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 6488976201c..99668eb74fc 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -332,22 +332,34 @@ function getSelectedModel({ const info = apiConfiguration?.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults return { id, info } } + // kilocode_change start - improved context window handling case "ollama": { const id = apiConfiguration.ollamaModelId ?? "" const info = ollamaModels && ollamaModels[apiConfiguration.ollamaModelId!] + const userContextWindow = apiConfiguration?.ollamaNumCtx - const adjustedInfo = - info?.contextWindow && - apiConfiguration?.ollamaNumCtx && - apiConfiguration.ollamaNumCtx < info.contextWindow - ? { ...info, contextWindow: apiConfiguration.ollamaNumCtx } - : info + // If user has set ollamaNumCtx, always use it as the context window (user's explicit setting takes precedence) + // If no user setting, use the fetched model info's context window + // If neither, provide a sensible default so UI doesn't show undefined + let adjustedInfo: ModelInfo | undefined + if (info) { + adjustedInfo = userContextWindow ? { ...info, contextWindow: userContextWindow } : info + } else if (userContextWindow) { + // No fetched model info but user has set context window - provide default model info with user's setting + adjustedInfo = { + maxTokens: userContextWindow, + contextWindow: userContextWindow, + supportsImages: true, + supportsPromptCache: true, + } + } return { id, info: adjustedInfo || undefined, } } + // kilocode_change end case "lmstudio": { const id = apiConfiguration.lmStudioModelId ?? "" const info = lmStudioModels && lmStudioModels[apiConfiguration.lmStudioModelId!]