Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/types/src/kilocode/kilocode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const ghostServiceSettingsSchema = z
enableAutoTrigger: z.boolean().optional(),
enableQuickInlineTaskKeybinding: z.boolean().optional(),
enableSmartInlineTaskKeybinding: z.boolean().optional(),
enableChatAutocomplete: z.boolean().optional(),
provider: z.string().optional(),
model: z.string().optional(),
})
Expand Down
12 changes: 12 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3539,7 +3539,19 @@ export const webviewMessageHandler = async (
await provider.postMessageToWebview({ type: "keybindingsResponse", keybindings: {} })
}
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,
getCurrentCwd,
)
break
}
// kilocode_change end: Chat text area FIM autocomplete
case "openCommandFile": {
try {
if (message.text) {
Expand Down
159 changes: 159 additions & 0 deletions src/services/ghost/chat-autocomplete/ChatTextAreaAutocomplete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import * as vscode from "vscode"
import { GhostModel } from "../GhostModel"
import { ProviderSettingsManager } from "../../../core/config/ProviderSettingsManager"
import { VisibleCodeContext } from "../types"

/**
* Service for providing FIM-based autocomplete suggestions in ChatTextArea
*/
export class ChatTextAreaAutocomplete {
private model: GhostModel
private providerSettingsManager: ProviderSettingsManager

constructor(providerSettingsManager: ProviderSettingsManager) {
this.model = new GhostModel()
this.providerSettingsManager = providerSettingsManager
}

async initialize(): Promise<boolean> {
return this.model.reload(this.providerSettingsManager)
}

isFimAvailable(): boolean {
return this.model.loaded && this.model.supportsFim()
}

async getCompletion(userText: string, visibleCodeContext?: VisibleCodeContext): Promise<{ suggestion: string }> {
if (!this.model.loaded) {
const loaded = await this.initialize()
if (!loaded) {
return { suggestion: "" }
}
}

if (!this.model.supportsFim()) {
console.log("[ChatTextAreaAutocomplete] FIM not supported by current model")
return { suggestion: "" }
}

const prefix = await this.buildPrefix(userText, visibleCodeContext)
const suffix = ""

console.log("[ChatTextAreaAutocomplete] === FIM Request ===")
console.log("[ChatTextAreaAutocomplete] User text:", JSON.stringify(userText))
console.log("[ChatTextAreaAutocomplete] Full prefix:\n", prefix)

let response = ""
await this.model.generateFimResponse(prefix, suffix, (chunk) => {
response += chunk
})

console.log("[ChatTextAreaAutocomplete] === FIM Response ===")
console.log("[ChatTextAreaAutocomplete] Raw response:", JSON.stringify(response))

const cleanedSuggestion = this.cleanSuggestion(response, userText)

console.log("[ChatTextAreaAutocomplete] Cleaned suggestion:", JSON.stringify(cleanedSuggestion))

return { suggestion: cleanedSuggestion }
}

/**
* Build the prefix for FIM completion with visible code context and additional sources
*/
private async buildPrefix(userText: string, visibleCodeContext?: VisibleCodeContext): Promise<string> {
const contextParts: string[] = []

// Add visible code context (replaces cursor-based prefix/suffix)
if (visibleCodeContext && visibleCodeContext.editors.length > 0) {
contextParts.push("// Code visible in editor:")

for (const editor of visibleCodeContext.editors) {
const fileName = editor.filePath.split("/").pop() || editor.filePath
contextParts.push(`\n// File: ${fileName} (${editor.languageId})`)

for (const range of editor.visibleRanges) {
contextParts.push(range.content)
}
}
}

const clipboardContent = await this.getClipboardContext()
if (clipboardContent) {
contextParts.push("\n// Clipboard content:")
contextParts.push(clipboardContent)
}

contextParts.push("\n// User's message:")
contextParts.push(userText)

return contextParts.join("\n")
}

/**
* Get clipboard content for context
*/
private async getClipboardContext(): Promise<string | null> {
try {
const text = await vscode.env.clipboard.readText()
// Only include if it's reasonable size and looks like code
if (text && text.length > 5 && text.length < 500) {
return text
}
} catch (error) {
console.log("[ChatTextAreaAutocomplete] Error getting clipboard:", error)
}
return null
}

/**
* Clean the suggestion by removing any leading repetition of user text
* and filtering out unwanted patterns like comments
*/
private cleanSuggestion(suggestion: string, userText: string): string {
let cleaned = suggestion.trim()

if (cleaned.startsWith(userText)) {
cleaned = cleaned.substring(userText.length)
}

const firstNewline = cleaned.indexOf("\n")
if (firstNewline !== -1) {
cleaned = cleaned.substring(0, firstNewline)
}

cleaned = cleaned.trimStart()

// Filter out suggestions that start with comment patterns
// This happens because the context uses // prefixes for labels
if (this.isUnwantedSuggestion(cleaned)) {
return ""
}

return cleaned
}

/**
* Check if suggestion should be filtered out
*/
public isUnwantedSuggestion(suggestion: string): boolean {
// Filter comment-starting suggestions
if (suggestion.startsWith("//") || suggestion.startsWith("/*") || suggestion.startsWith("*")) {
return true
}

// Filter suggestions that look like code rather than natural language
// These patterns indicate the model is completing code, not a user message
if (suggestion.startsWith("#") && !suggestion.startsWith("# ")) {
// Allow "# " as it might be markdown header, but filter "#include" etc.
return true
}

// Filter suggestions that are just punctuation or whitespace
if (suggestion.length < 2 || /^[\s\p{P}]+$/u.test(suggestion)) {
return true
}

return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { ChatTextAreaAutocomplete } from "../ChatTextAreaAutocomplete"
import { ProviderSettingsManager } from "../../../../core/config/ProviderSettingsManager"

describe("ChatTextAreaAutocomplete", () => {
let autocomplete: ChatTextAreaAutocomplete
let mockProviderSettingsManager: ProviderSettingsManager

beforeEach(() => {
// Create a minimal mock for ProviderSettingsManager
mockProviderSettingsManager = {} as ProviderSettingsManager
autocomplete = new ChatTextAreaAutocomplete(mockProviderSettingsManager)
})

describe("isUnwantedSuggestion", () => {
// Helper function to test filtering (returns true)
const shouldFilter = (suggestion: string) => {
return autocomplete.isUnwantedSuggestion(suggestion)
}

// Helper function to test acceptance (returns false)
const shouldAccept = (suggestion: string) => {
return !autocomplete.isUnwantedSuggestion(suggestion)
}

describe("should filter comment-starting suggestions", () => {
it("should filter single-line comments", () => {
expect(shouldFilter("// comment")).toBe(true)
expect(shouldFilter("//")).toBe(true)
expect(shouldFilter("// some text")).toBe(true)
})

it("should filter multi-line comment starts", () => {
expect(shouldFilter("/* comment")).toBe(true)
expect(shouldFilter("/**")).toBe(true)
expect(shouldFilter("/*")).toBe(true)
})

it("should filter asterisk-only suggestions", () => {
expect(shouldFilter("*")).toBe(true)
expect(shouldFilter("* comment")).toBe(true)
expect(shouldFilter("**")).toBe(true)
})
})

describe("should filter code-like patterns", () => {
it("should filter preprocessor directives", () => {
expect(shouldFilter("#include")).toBe(true)
expect(shouldFilter("#define")).toBe(true)
expect(shouldFilter("#ifdef")).toBe(true)
})

it("should allow markdown headers", () => {
expect(shouldAccept("# ")).toBe(true)
expect(shouldAccept("# Header")).toBe(true)
expect(shouldAccept("# Title here")).toBe(true)
})
})

describe("should filter punctuation and whitespace", () => {
it("should filter short suggestions", () => {
expect(shouldFilter("")).toBe(true)
expect(shouldFilter(" ")).toBe(true)
expect(shouldFilter("a")).toBe(true)
})

it("should filter punctuation-only suggestions", () => {
expect(shouldFilter(".,")).toBe(true)
expect(shouldFilter("!?")).toBe(true)
expect(shouldFilter("...")).toBe(true)
expect(shouldFilter(" ")).toBe(true)
})

it("should filter mixed punctuation and whitespace", () => {
expect(shouldFilter(" , ")).toBe(true)
expect(shouldFilter("... ")).toBe(true)
})
})

describe("should accept valid suggestions", () => {
it("should accept natural language text", () => {
expect(shouldAccept("Hello world")).toBe(true)
expect(shouldAccept("This is a valid suggestion")).toBe(true)
expect(shouldAccept("Can you help me with")).toBe(true)
})

it("should accept suggestions starting with letters", () => {
expect(shouldAccept("test")).toBe(true)
expect(shouldAccept("function")).toBe(true)
expect(shouldAccept("variable")).toBe(true)
})

it("should accept suggestions with numbers", () => {
expect(shouldAccept("123")).toBe(true)
expect(shouldAccept("test123")).toBe(true)
expect(shouldAccept("42 is the answer")).toBe(true)
})

it("should accept markdown headers", () => {
expect(shouldAccept("# Header")).toBe(true)
expect(shouldAccept("## Subheader")).toBe(true)
})

it("should accept suggestions with punctuation in the middle", () => {
expect(shouldAccept("Hello, world")).toBe(true)
expect(shouldAccept("What's up?")).toBe(true)
expect(shouldAccept("Let's go!")).toBe(true)
})
})

describe("edge cases", () => {
it("should handle unicode characters", () => {
expect(shouldAccept("你好")).toBe(true)
expect(shouldAccept("🚀")).toBe(true)
expect(shouldFilter("🚀")).toBe(false) // Single emoji is 2+ chars, should be accepted
})

it("should handle very long suggestions", () => {
const longSuggestion = "a".repeat(1000)
expect(shouldAccept(longSuggestion)).toBe(true)
})

it("should handle mixed content", () => {
expect(shouldAccept("Hello // but not a comment")).toBe(true)
expect(shouldAccept("Text with # in middle")).toBe(true)
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ClineProvider } from "../../../core/webview/ClineProvider"
import { WebviewMessage } from "../../../shared/WebviewMessage"
import { VisibleCodeTracker } from "../context/VisibleCodeTracker"
import { ChatTextAreaAutocomplete } from "./ChatTextAreaAutocomplete"

/**
* Handles a chat completion request from the webview.
* Captures visible code context and generates a FIM-based autocomplete suggestion.
*/
export async function handleChatCompletionRequest(
message: WebviewMessage & { type: "requestChatCompletion" },
provider: ClineProvider,
getCurrentCwd: () => string,
): Promise<void> {
try {
const userText = message.text || ""
const requestId = message.requestId || ""

const tracker = new VisibleCodeTracker(getCurrentCwd())
const visibleContext = await tracker.captureVisibleCode()

const autocomplete = new ChatTextAreaAutocomplete(provider.providerSettingsManager)
const { suggestion } = await autocomplete.getCompletion(userText, visibleContext)

await provider.postMessageToWebview({ type: "chatCompletionResult", text: suggestion, requestId })
} catch (error) {
provider.log(`Error getting chat completion: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
await provider.postMessageToWebview({
type: "chatCompletionResult",
text: "",
requestId: message.requestId || "",
})
}
}
Loading
Loading