diff --git a/packages/types/src/kilocode/kilocode.ts b/packages/types/src/kilocode/kilocode.ts index 9b5bb988e96..02d2a9342aa 100644 --- a/packages/types/src/kilocode/kilocode.ts +++ b/packages/types/src/kilocode/kilocode.ts @@ -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(), }) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 22395dc6a5a..94c68955561 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -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) { diff --git a/src/services/ghost/chat-autocomplete/ChatTextAreaAutocomplete.ts b/src/services/ghost/chat-autocomplete/ChatTextAreaAutocomplete.ts new file mode 100644 index 00000000000..e1295b30f7b --- /dev/null +++ b/src/services/ghost/chat-autocomplete/ChatTextAreaAutocomplete.ts @@ -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 { + 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 { + 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 { + 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 + } +} diff --git a/src/services/ghost/chat-autocomplete/__tests__/ChatTextAreaAutocomplete.test.ts b/src/services/ghost/chat-autocomplete/__tests__/ChatTextAreaAutocomplete.test.ts new file mode 100644 index 00000000000..e90152f5fd1 --- /dev/null +++ b/src/services/ghost/chat-autocomplete/__tests__/ChatTextAreaAutocomplete.test.ts @@ -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) + }) + }) + }) +}) diff --git a/src/services/ghost/chat-autocomplete/handleChatCompletionRequest.ts b/src/services/ghost/chat-autocomplete/handleChatCompletionRequest.ts new file mode 100644 index 00000000000..2d286562301 --- /dev/null +++ b/src/services/ghost/chat-autocomplete/handleChatCompletionRequest.ts @@ -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 { + 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 || "", + }) + } +} diff --git a/src/services/ghost/context/VisibleCodeTracker.ts b/src/services/ghost/context/VisibleCodeTracker.ts new file mode 100644 index 00000000000..68cba7c33ff --- /dev/null +++ b/src/services/ghost/context/VisibleCodeTracker.ts @@ -0,0 +1,140 @@ +/** + * VisibleCodeTracker - Captures the actual visible code in VS Code editors + * + * This service captures what code is currently visible on the user's screen, + * not just what files are open. It uses the VS Code API to get: + * - All visible text editors (not just tabs) + * - The actual visible line ranges in each editor's viewport + * - Cursor positions and selections + */ + +import * as vscode from "vscode" + +import { toRelativePath } from "../../../utils/path" + +import { VisibleCodeContext, VisibleEditorInfo, VisibleRange, DiffInfo } from "../types" + +// Git-related URI schemes that should be captured for diff support +const GIT_SCHEMES = ["git", "gitfs", "file", "vscode-remote"] + +export class VisibleCodeTracker { + private lastContext: VisibleCodeContext | null = null + + constructor(private workspacePath: string) {} + + /** + * Captures the currently visible code across all visible editors. + * + * @returns VisibleCodeContext containing information about all visible editors + * and their visible code ranges + */ + public async captureVisibleCode(): Promise { + const editors = vscode.window.visibleTextEditors + const activeUri = vscode.window.activeTextEditor?.document.uri.toString() + + const editorInfos: VisibleEditorInfo[] = [] + + for (const editor of editors) { + const document = editor.document + const scheme = document.uri.scheme + + // Skip non-code documents (output panels, extension host output, etc.) + if (!GIT_SCHEMES.includes(scheme)) { + continue + } + + const visibleRanges: VisibleRange[] = [] + + for (const range of editor.visibleRanges) { + const content = document.getText(range) + visibleRanges.push({ + startLine: range.start.line, + endLine: range.end.line, + content, + }) + } + + const isActive = document.uri.toString() === activeUri + + // Extract diff information for git-backed documents + const diffInfo = this.extractDiffInfo(document.uri) + + editorInfos.push({ + filePath: document.uri.fsPath, + relativePath: toRelativePath(document.uri.fsPath, this.workspacePath), + languageId: document.languageId, + isActive, + visibleRanges, + cursorPosition: editor.selection + ? { + line: editor.selection.active.line, + character: editor.selection.active.character, + } + : null, + selections: editor.selections.map((sel) => ({ + start: { line: sel.start.line, character: sel.start.character }, + end: { line: sel.end.line, character: sel.end.character }, + })), + diffInfo, + }) + } + + this.lastContext = { + timestamp: Date.now(), + editors: editorInfos, + } + + // Log a brief summary + const fileNames = editorInfos.map((e) => e.relativePath).join(", ") + console.log(`[VisibleCodeTracker] Captured ${editorInfos.length} editor(s): ${fileNames || "(none)"}`) + + return this.lastContext + } + + /** + * Returns the last captured context, or null if never captured. + */ + public getLastContext(): VisibleCodeContext | null { + return this.lastContext + } + + /** + * Extract diff information from a URI. + * Git URIs typically look like: git:/path/to/file.ts?ref=HEAD~1 + */ + private extractDiffInfo(uri: vscode.Uri): DiffInfo | undefined { + const scheme = uri.scheme + + // Only extract diff info for git-related schemes + if (scheme === "git" || scheme === "gitfs") { + // Parse query parameters for git reference + const query = uri.query + let gitRef: string | undefined + + if (query) { + // Common patterns: ref=HEAD, ref=abc123 + const refMatch = query.match(/ref=([^&]+)/) + if (refMatch) { + gitRef = refMatch[1] + } + } + + return { + scheme, + side: "old", // Git scheme documents are typically the "old" side + gitRef, + originalPath: uri.fsPath, + } + } + + // File scheme in a diff view is the "new" side + // We can't always tell if it's in a diff, so we mark it as new when there's a paired git doc + if (scheme === "file") { + // This will be marked as diffInfo only if we detect it's paired with a git document + // For now, we don't set diffInfo for regular file scheme documents + return undefined + } + + return undefined + } +} diff --git a/src/services/ghost/context/__tests__/VisibleCodeTracker.test.ts b/src/services/ghost/context/__tests__/VisibleCodeTracker.test.ts new file mode 100644 index 00000000000..3cb73de0f6a --- /dev/null +++ b/src/services/ghost/context/__tests__/VisibleCodeTracker.test.ts @@ -0,0 +1,164 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import * as vscode from "vscode" +import { VisibleCodeTracker } from "../VisibleCodeTracker" + +// Mock vscode module +vi.mock("vscode", () => ({ + window: { + visibleTextEditors: [], + activeTextEditor: null, + }, +})) + +describe("VisibleCodeTracker", () => { + const mockWorkspacePath = "/workspace" + + beforeEach(() => { + // Reset mocks before each test + vi.clearAllMocks() + }) + + describe("captureVisibleCode", () => { + it("should return empty context when no editors are visible", async () => { + // Mock empty editor list + ;(vscode.window.visibleTextEditors as any) = [] + ;(vscode.window.activeTextEditor as any) = null + + const tracker = new VisibleCodeTracker(mockWorkspacePath) + const context = await tracker.captureVisibleCode() + + expect(context).toEqual({ + timestamp: expect.any(Number), + editors: [], + }) + expect(context.timestamp).toBeGreaterThan(0) + }) + + it("should capture visible editors with file scheme", async () => { + const mockDocument = { + uri: { + fsPath: "/workspace/test.ts", + scheme: "file", + toString: () => "file:///workspace/test.ts", + }, + languageId: "typescript", + getText: vi.fn((range: any) => { + if (range.start.line === 0 && range.end.line === 2) { + return "line 0\nline 1\nline 2" + } + return "" + }), + } + + const mockEditor = { + document: mockDocument, + visibleRanges: [ + { + start: { line: 0, character: 0 }, + end: { line: 2, character: 0 }, + }, + ], + selection: { + active: { line: 1, character: 5 }, + }, + selections: [ + { + start: { line: 1, character: 0 }, + end: { line: 1, character: 10 }, + }, + ], + } + + ;(vscode.window.visibleTextEditors as any) = [mockEditor] + ;(vscode.window.activeTextEditor as any) = mockEditor + + const tracker = new VisibleCodeTracker(mockWorkspacePath) + const context = await tracker.captureVisibleCode() + + expect(context.editors).toHaveLength(1) + expect(context.editors[0]).toMatchObject({ + filePath: "/workspace/test.ts", + relativePath: "test.ts", + languageId: "typescript", + isActive: true, + visibleRanges: [ + { + startLine: 0, + endLine: 2, + content: "line 0\nline 1\nline 2", + }, + ], + cursorPosition: { + line: 1, + character: 5, + }, + }) + }) + + it("should extract diff info for git scheme URIs", async () => { + const mockDocument = { + uri: { + fsPath: "/workspace/test.ts", + scheme: "git", + query: "ref=HEAD~1", + toString: () => "git:///workspace/test.ts?ref=HEAD~1", + }, + languageId: "typescript", + getText: vi.fn(() => "old content"), + } + + const mockEditor = { + document: mockDocument, + visibleRanges: [ + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, + }, + ], + selection: null, + selections: [], + } + + ;(vscode.window.visibleTextEditors as any) = [mockEditor] + ;(vscode.window.activeTextEditor as any) = null + + const tracker = new VisibleCodeTracker(mockWorkspacePath) + const context = await tracker.captureVisibleCode() + + expect(context.editors[0].diffInfo).toEqual({ + scheme: "git", + side: "old", + gitRef: "HEAD~1", + originalPath: "/workspace/test.ts", + }) + }) + + it("should skip non-code documents", async () => { + const mockOutputDocument = { + uri: { + fsPath: "/workspace/output", + scheme: "output", + toString: () => "output:///workspace/output", + }, + languageId: "plaintext", + getText: vi.fn(() => ""), + } + + const mockEditor = { + document: mockOutputDocument, + visibleRanges: [], + selection: null, + selections: [], + } + + ;(vscode.window.visibleTextEditors as any) = [mockEditor] + ;(vscode.window.activeTextEditor as any) = null + + const tracker = new VisibleCodeTracker(mockWorkspacePath) + const context = await tracker.captureVisibleCode() + + // Output scheme should be filtered out + expect(context.editors).toHaveLength(0) + }) + }) +}) diff --git a/src/services/ghost/types.ts b/src/services/ghost/types.ts index e1dfdcfb492..54d7e64e721 100644 --- a/src/services/ghost/types.ts +++ b/src/services/ghost/types.ts @@ -137,6 +137,84 @@ export interface PromptResult { completionId: string } +// ============================================================================ +// Visible Code Context Types +// ============================================================================ + +/** + * Visible range in an editor viewport + */ +export interface VisibleRange { + startLine: number + endLine: number + content: string +} + +/** + * Diff metadata for git-backed editors + */ +export interface DiffInfo { + /** The URI scheme (e.g., "git", "gitfs") */ + scheme: string + /** Whether this is the "old" (left) or "new" (right) side of a diff */ + side: "old" | "new" + /** Git reference if available (e.g., "HEAD", "HEAD~1", commit hash) */ + gitRef?: string + /** The actual file path being compared */ + originalPath: string +} + +/** + * Information about a visible editor + */ +export interface VisibleEditorInfo { + /** Absolute file path */ + filePath: string + /** Path relative to workspace */ + relativePath: string + /** Language identifier (e.g., "typescript", "python") */ + languageId: string + /** Whether this is the active editor */ + isActive: boolean + /** The visible line ranges in the editor viewport */ + visibleRanges: VisibleRange[] + /** Current cursor position, or null if no cursor */ + cursorPosition: Position | null + /** All selections in the editor */ + selections: Range[] + /** Diff information if this editor is part of a diff view */ + diffInfo?: DiffInfo +} + +/** + * Context of all visible code in editors + */ +export interface VisibleCodeContext { + /** Timestamp when the context was captured */ + timestamp: number + /** Information about all visible editors */ + editors: VisibleEditorInfo[] +} + +// ============================================================================ +// Chat Autocomplete Types +// ============================================================================ + +/** + * Request for chat text area completion + */ +export interface ChatCompletionRequest { + text: string +} + +/** + * Result of chat text area completion + */ +export interface ChatCompletionResult { + suggestion: string + requestId: string +} + // ============================================================================ // Conversion Utilities // ============================================================================ diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 79a68504e58..c9aaa2e9b32 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -180,6 +180,7 @@ export interface ExtensionMessage { | "managedIndexerState" // kilocode_change | "singleCompletionResult" // kilocode_change | "managedIndexerState" // kilocode_change + | "chatCompletionResult" // kilocode_change: FIM completion result for chat text area text?: string // kilocode_change start completionRequestId?: string // Correlation ID from request diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 5815fed6a95..f8fa423e91d 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -261,6 +261,7 @@ export interface WebviewMessage { | "shareTaskSession" // kilocode_change | "sessionFork" // kilocode_change | "singleCompletion" // kilocode_change + | "requestChatCompletion" // kilocode_change: Request FIM completion for chat text area text?: string completionRequestId?: string // kilocode_change shareId?: string // kilocode_change - for sessionFork diff --git a/src/shared/id.ts b/src/shared/id.ts new file mode 100644 index 00000000000..e6317b74170 --- /dev/null +++ b/src/shared/id.ts @@ -0,0 +1,7 @@ +/** + * Generate a unique request ID for messages. + * Uses a short random string suitable for request correlation. + */ +export function generateRequestId(): string { + return Math.random().toString(36).substring(2, 9) +} diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index c8d3d4ef014..0e385f25d40 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -20,6 +20,8 @@ import { SearchResult, } from "@src/utils/context-mentions" import { convertToMentionPath } from "@/utils/path-mentions" +import { escapeHtml } from "@/utils/highlight" // kilocode_change - FIM autocomplete +import { useChatGhostText } from "./hooks/useChatGhostText" // kilocode_change: FIM autocomplete import { DropdownOptionType, Button, StandardTooltip } from "@/components/ui" // kilocode_change import Thumbnails from "../common/Thumbnails" @@ -142,6 +144,7 @@ export const ChatTextArea = forwardRef( globalWorkflows, // kilocode_change taskHistoryVersion, // kilocode_change clineMessages, + ghostServiceSettings, // kilocode_change } = useExtensionState() // kilocode_change start - autocomplete profile type system @@ -293,6 +296,18 @@ export const ChatTextArea = forwardRef( const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false) const [isFocused, setIsFocused] = useState(false) const [imageWarning, setImageWarning] = useState(null) // kilocode_change + // kilocode_change start: FIM autocomplete ghost text + const { + ghostText, + handleKeyDown: handleGhostTextKeyDown, + handleInputChange: handleGhostTextInputChange, + } = useChatGhostText({ + inputValue, + setInputValue, + textAreaRef, + enableChatAutocomplete: ghostServiceSettings?.enableChatAutocomplete ?? false, + }) + // kilocode_change end: FIM autocomplete ghost text // Use custom hook for prompt history navigation const { handleHistoryNavigation, resetHistoryNavigation, resetOnInputChange } = usePromptHistory({ @@ -520,6 +535,12 @@ export const ChatTextArea = forwardRef( const handleKeyDown = useCallback( (event: React.KeyboardEvent) => { + // kilocode_change start: FIM autocomplete - Tab to accept ghost text + if (handleGhostTextKeyDown(event)) { + return // Event was handled by ghost text hook, stop here + } + // kilocode_change end: FIM autocomplete + // kilocode_change start: pull slash commands from Cline if (showSlashCommandsMenu) { if (event.key === "Escape") { @@ -710,6 +731,7 @@ export const ChatTextArea = forwardRef( handleSlashCommandsSelect, selectedSlashCommandsIndex, slashCommandsQuery, + handleGhostTextKeyDown, // kilocode_change: FIM autocomplete // kilocode_change end onSend, showContextMenu, @@ -748,6 +770,8 @@ export const ChatTextArea = forwardRef( // Reset history navigation when user types resetOnInputChange() + handleGhostTextInputChange(e) // kilocode_change - FIM autocomplete + const newCursorPosition = e.target.selectionStart setCursorPosition(newCursorPosition) @@ -829,7 +853,14 @@ export const ChatTextArea = forwardRef( setFileSearchResults([]) // Clear file search results. } }, - [setInputValue, setSearchRequestId, setFileSearchResults, setSearchLoading, resetOnInputChange], + [ + setInputValue, + setSearchRequestId, + setFileSearchResults, + setSearchLoading, + resetOnInputChange, + handleGhostTextInputChange, // kilocode_change: FIM autocomplete + ], ) useEffect(() => { @@ -982,11 +1013,15 @@ export const ChatTextArea = forwardRef( } } // kilocode_change end + if (ghostText) { + processedText += `${escapeHtml(ghostText)}` + } + // kilocode_change end: FIM autocomplete ghost text display highlightLayerRef.current.innerHTML = processedText highlightLayerRef.current.scrollTop = textAreaRef.current.scrollTop highlightLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft - }, [customModes]) + }, [customModes, ghostText]) useLayoutEffect(() => { updateHighlights() diff --git a/webview-ui/src/components/chat/hooks/useChatGhostText.ts b/webview-ui/src/components/chat/hooks/useChatGhostText.ts new file mode 100644 index 00000000000..4cc296f25bd --- /dev/null +++ b/webview-ui/src/components/chat/hooks/useChatGhostText.ts @@ -0,0 +1,141 @@ +// kilocode_change - new file +import { useCallback, useEffect, useRef, useState } from "react" +import { ExtensionMessage } from "@roo/ExtensionMessage" +import { vscode } from "@/utils/vscode" +import { generateRequestId } from "@roo/id" + +interface UseChatGhostTextOptions { + inputValue: string + setInputValue: (value: string) => void + textAreaRef: React.RefObject + enableChatAutocomplete?: boolean +} + +interface UseChatGhostTextReturn { + ghostText: string + handleKeyDown: (event: React.KeyboardEvent) => boolean // Returns true if event was handled + handleInputChange: (e: React.ChangeEvent) => void + clearGhostText: () => void +} + +/** + * Hook for managing FIM autocomplete ghost text in the chat text area. + * Handles completion requests, ghost text display, and Tab/Escape interactions. + */ +export function useChatGhostText({ + inputValue, + setInputValue, + textAreaRef, + enableChatAutocomplete = false, +}: UseChatGhostTextOptions): UseChatGhostTextReturn { + const [ghostText, setGhostText] = useState("") + const completionDebounceRef = useRef(null) + const completionRequestIdRef = useRef("") + const skipNextCompletionRef = useRef(false) // Skip completion after accepting suggestion + + // Handle chat completion result messages + useEffect(() => { + const messageHandler = (event: MessageEvent) => { + const message = event.data + if (message.type === "chatCompletionResult") { + // Only update if this is the response to our latest request + if (message.requestId === completionRequestIdRef.current) { + setGhostText(message.text || "") + } + } + } + + window.addEventListener("message", messageHandler) + return () => window.removeEventListener("message", messageHandler) + }, []) + + const clearGhostText = useCallback(() => { + setGhostText("") + }, []) + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent): boolean => { + // Tab to accept ghost text + if (event.key === "Tab" && ghostText && !event.shiftKey) { + event.preventDefault() + // Skip the next completion request since we just accepted a suggestion + skipNextCompletionRef.current = true + try { + // Use execCommand to insert text while preserving undo history + if (document.execCommand && textAreaRef.current) { + const textarea = textAreaRef.current + // Move cursor to end and insert the ghost text + textarea.setSelectionRange(textarea.value.length, textarea.value.length) + document.execCommand("insertText", false, ghostText) + } else { + setInputValue(inputValue + ghostText) + } + } catch { + setInputValue(inputValue + ghostText) + } + setGhostText("") + return true // Event was handled, stop propagation + } + // Clear ghost text on Escape + if (event.key === "Escape" && ghostText) { + setGhostText("") + // Don't return true - let other handlers process Escape too + } + return false // Event was not handled by this hook + }, + [ghostText, inputValue, setInputValue, textAreaRef], + ) + + const handleInputChange = useCallback( + (e: React.ChangeEvent) => { + const newValue = e.target.value + + // Clear any existing ghost text when typing + setGhostText("") + + // Clear any pending completion request + if (completionDebounceRef.current) { + clearTimeout(completionDebounceRef.current) + } + + // Skip completion request if we just accepted a suggestion (Tab) or undid + if (skipNextCompletionRef.current) { + skipNextCompletionRef.current = false + // Don't request a new completion - wait for user to type more + } else if ( + enableChatAutocomplete && + newValue.length >= 5 && + !newValue.startsWith("/") && + !newValue.includes("@") + ) { + // Request new completion after debounce (only if feature is enabled) + completionDebounceRef.current = setTimeout(() => { + const requestId = generateRequestId() + completionRequestIdRef.current = requestId + vscode.postMessage({ + type: "requestChatCompletion", + text: newValue, + requestId, + }) + }, 300) // 300ms debounce + } + }, + [enableChatAutocomplete], + ) + + // Cleanup on unmount + useEffect(() => { + return () => { + if (completionDebounceRef.current) { + clearTimeout(completionDebounceRef.current) + } + } + }, []) + + return { + ghostText, + handleKeyDown, + handleInputChange, + clearGhostText, + } +} diff --git a/webview-ui/src/components/kilocode/settings/GhostServiceSettings.tsx b/webview-ui/src/components/kilocode/settings/GhostServiceSettings.tsx index 58caa769b3a..c6c22bd073b 100644 --- a/webview-ui/src/components/kilocode/settings/GhostServiceSettings.tsx +++ b/webview-ui/src/components/kilocode/settings/GhostServiceSettings.tsx @@ -26,8 +26,14 @@ export const GhostServiceSettingsView = ({ ...props }: GhostServiceSettingsViewProps) => { const { t } = useAppTranslation() - const { enableAutoTrigger, enableQuickInlineTaskKeybinding, enableSmartInlineTaskKeybinding, provider, model } = - ghostServiceSettings || {} + const { + enableAutoTrigger, + enableQuickInlineTaskKeybinding, + enableSmartInlineTaskKeybinding, + enableChatAutocomplete, + provider, + model, + } = ghostServiceSettings || {} const keybindings = useKeybindings(["kilo-code.addToContextAndFocus", "kilo-code.ghost.generateSuggestions"]) const onEnableAutoTriggerChange = useCallback( @@ -51,6 +57,13 @@ export const GhostServiceSettingsView = ({ [onGhostServiceSettingsChange], ) + const onEnableChatAutocompleteChange = useCallback( + (e: any) => { + onGhostServiceSettingsChange("enableChatAutocomplete", e.target.checked) + }, + [onGhostServiceSettingsChange], + ) + const openGlobalKeybindings = (filter?: string) => { vscode.postMessage({ type: "openGlobalKeybindings", text: filter }) } @@ -132,6 +145,19 @@ export const GhostServiceSettingsView = ({ +
+ + + {t("kilocode:ghost.settings.enableChatAutocomplete.label")} + + +
+ +
+
+
diff --git a/webview-ui/src/components/kilocode/settings/__tests__/GhostServiceSettings.spec.tsx b/webview-ui/src/components/kilocode/settings/__tests__/GhostServiceSettings.spec.tsx index 334cb27c641..b926ba81384 100644 --- a/webview-ui/src/components/kilocode/settings/__tests__/GhostServiceSettings.spec.tsx +++ b/webview-ui/src/components/kilocode/settings/__tests__/GhostServiceSettings.spec.tsx @@ -88,6 +88,7 @@ const defaultGhostServiceSettings: GhostServiceSettings = { enableAutoTrigger: false, enableQuickInlineTaskKeybinding: false, enableSmartInlineTaskKeybinding: false, + enableChatAutocomplete: false, provider: "openrouter", model: "openai/gpt-4o-mini", } @@ -170,6 +171,18 @@ describe("GhostServiceSettingsView", () => { expect(onGhostServiceSettingsChange).toHaveBeenCalledWith("enableSmartInlineTaskKeybinding", true) }) + it("toggles chat autocomplete checkbox correctly", () => { + const onGhostServiceSettingsChange = vi.fn() + renderComponent({ onGhostServiceSettingsChange }) + + const checkboxLabel = screen.getByText(/kilocode:ghost.settings.enableChatAutocomplete.label/).closest("label") + const checkbox = checkboxLabel?.querySelector('input[type="checkbox"]') as HTMLInputElement + + fireEvent.click(checkbox) + + expect(onGhostServiceSettingsChange).toHaveBeenCalledWith("enableChatAutocomplete", true) + }) + it("renders Trans components with proper structure", () => { renderComponent() diff --git a/webview-ui/src/i18n/locales/ar/kilocode.json b/webview-ui/src/i18n/locales/ar/kilocode.json index 5333a322808..16cc35981a1 100644 --- a/webview-ui/src/i18n/locales/ar/kilocode.json +++ b/webview-ui/src/i18n/locales/ar/kilocode.json @@ -234,7 +234,11 @@ "noModelConfigured": "لم يتم العثور على نموذج إكمال تلقائي مناسب. يرجى تكوين مزود في إعدادات API.", "configureAutocompleteProfile": "استخدم أي نموذج بالانتقال إلى الملفات الشخصية وتكوين ملف شخصي من نوع الإكمال التلقائي.", "model": "النموذج", - "provider": "المزود" + "provider": "المزود", + "enableChatAutocomplete": { + "label": "الإكمال التلقائي لمدخلات الدردشة", + "description": "عند التفعيل، سيقترح Kilo Code إكمالات أثناء كتابتك في مدخل الدردشة. اضغط على Tab لقبول الاقتراحات." + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/ca/kilocode.json b/webview-ui/src/i18n/locales/ca/kilocode.json index c54d3a669db..d66fe53af6c 100644 --- a/webview-ui/src/i18n/locales/ca/kilocode.json +++ b/webview-ui/src/i18n/locales/ca/kilocode.json @@ -225,7 +225,11 @@ "noModelConfigured": "No s'ha trobat cap model d'autocompletat adequat. Configura un proveïdor a la configuració de l'API.", "configureAutocompleteProfile": "Utilitza qualsevol model anant a perfils i configurant un Perfil del Tipus de Perfil Autocompletat.", "model": "Model", - "provider": "Proveïdor" + "provider": "Proveïdor", + "enableChatAutocomplete": { + "description": "Quan està activat, Kilo Code suggerirà complecions mentre escriviu a l'entrada del xat. Premeu Tab per acceptar els suggeriments.", + "label": "Autocompletat d'entrada de xat" + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/cs/kilocode.json b/webview-ui/src/i18n/locales/cs/kilocode.json index 6b23a679c92..f11a8742ea2 100644 --- a/webview-ui/src/i18n/locales/cs/kilocode.json +++ b/webview-ui/src/i18n/locales/cs/kilocode.json @@ -239,7 +239,11 @@ "noModelConfigured": "Nebyl nalezen žádný vhodný model pro automatické dokončování. Nakonfiguruj prosím poskytovatele v nastavení API.", "configureAutocompleteProfile": "Použij libovolný model tak, že přejdeš do profilů a nakonfiguruješ Profil typu Automatické dokončování.", "model": "Model", - "provider": "Poskytovatel" + "provider": "Poskytovatel", + "enableChatAutocomplete": { + "label": "Automatické doplňování při psaní zpráv", + "description": "Když je tato funkce povolena, Kilo Code bude navrhovat dokončování textu během psaní ve vstupním poli chatu. Stisknutím klávesy Tab přijmete návrhy." + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/de/kilocode.json b/webview-ui/src/i18n/locales/de/kilocode.json index 0f943ded919..846fb5f8a66 100644 --- a/webview-ui/src/i18n/locales/de/kilocode.json +++ b/webview-ui/src/i18n/locales/de/kilocode.json @@ -231,7 +231,11 @@ }, "keybindingNotFound": "nicht gefunden", "noModelConfigured": "Kein geeignetes Autocomplete-Modell gefunden. Bitte konfiguriere einen Provider in den API-Einstellungen.", - "configureAutocompleteProfile": "Verwende ein beliebiges Modell, indem du zu Profilen gehst und ein Profil vom Profiltyp Autocomplete konfigurierst." + "configureAutocompleteProfile": "Verwende ein beliebiges Modell, indem du zu Profilen gehst und ein Profil vom Profiltyp Autocomplete konfigurierst.", + "enableChatAutocomplete": { + "label": "Chat-Eingabe-Autovervollständigung", + "description": "Wenn aktiviert, wird Kilo Code Vervollständigungen vorschlagen, während Sie im Chat-Eingabefeld tippen. Drücken Sie Tab, um Vorschläge zu akzeptieren." + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/en/kilocode.json b/webview-ui/src/i18n/locales/en/kilocode.json index 0b4860c60ed..187da50e26e 100644 --- a/webview-ui/src/i18n/locales/en/kilocode.json +++ b/webview-ui/src/i18n/locales/en/kilocode.json @@ -243,6 +243,10 @@ "label": "Manual Autocomplete ({{keybinding}})", "description": "Need a quick fix, completion, or refactor? Kilo will use the surrounding context to offer immediate improvements, keeping you in the flow. Edit shortcut" }, + "enableChatAutocomplete": { + "label": "Chat Input Autocomplete", + "description": "When enabled, Kilo Code will suggest completions as you type in the chat input. Press Tab to accept suggestions." + }, "keybindingNotFound": "not found", "noModelConfigured": "No suitable autocomplete model found. Please configure a provider in the API settings.", "configureAutocompleteProfile": "Use any model by going to profiles and configuring a Profile of the Profile Type Autocomplete." diff --git a/webview-ui/src/i18n/locales/es/kilocode.json b/webview-ui/src/i18n/locales/es/kilocode.json index c8d716eb7a6..2753992e122 100644 --- a/webview-ui/src/i18n/locales/es/kilocode.json +++ b/webview-ui/src/i18n/locales/es/kilocode.json @@ -232,7 +232,11 @@ "noModelConfigured": "No se encontró ningún modelo de autocompletado adecuado. Por favor, configura un proveedor en la configuración de API.", "configureAutocompleteProfile": "Usa cualquier modelo yendo a perfiles y configurando un Perfil del Tipo de Perfil Autocompletado.", "model": "Modelo", - "provider": "Proveedor" + "provider": "Proveedor", + "enableChatAutocomplete": { + "description": "Cuando está habilitado, Kilo Code sugerirá completaciones mientras escribes en el campo de chat. Presiona Tab para aceptar las sugerencias.", + "label": "Autocompletado de Entrada de Chat" + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/fr/kilocode.json b/webview-ui/src/i18n/locales/fr/kilocode.json index f619cdf4e87..cf5ff81117b 100644 --- a/webview-ui/src/i18n/locales/fr/kilocode.json +++ b/webview-ui/src/i18n/locales/fr/kilocode.json @@ -239,7 +239,11 @@ "noModelConfigured": "Aucun modèle d'autocomplétion approprié trouvé. Configure un fournisseur dans les paramètres API.", "configureAutocompleteProfile": "Utilise n'importe quel modèle en allant dans les profils et en configurant un Profil du Type de Profil Autocomplétion.", "model": "Modèle", - "provider": "Fournisseur" + "provider": "Fournisseur", + "enableChatAutocomplete": { + "label": "Saisie automatique de discussion", + "description": "Lorsqu'il est activé, Kilo Code proposera des suggestions de complétion pendant que vous tapez dans le champ de discussion. Appuyez sur Tab pour accepter les suggestions." + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/hi/kilocode.json b/webview-ui/src/i18n/locales/hi/kilocode.json index bd37201dbd0..8ffab7ae49d 100644 --- a/webview-ui/src/i18n/locales/hi/kilocode.json +++ b/webview-ui/src/i18n/locales/hi/kilocode.json @@ -225,7 +225,11 @@ "noModelConfigured": "कोई उपयुक्त ऑटोकम्पलीट मॉडल नहीं मिला। कृपया API सेटिंग्स में एक प्रदाता कॉन्फ़िगर करें।", "configureAutocompleteProfile": "प्रोफाइल में जाकर और ऑटोकम्पलीट प्रोफाइल टाइप की एक प्रोफाइल कॉन्फ़िगर करके किसी भी मॉडल का उपयोग करें।", "model": "मॉडल", - "provider": "प्रदाता" + "provider": "प्रदाता", + "enableChatAutocomplete": { + "label": "चैट इनपुट स्वतःपूर्ण", + "description": "जब सक्षम होता है, Kilo Code चैट इनपुट में टाइप करते समय पूर्णताएं सुझाएगा। सुझावों को स्वीकार करने के लिए Tab दबाएं।" + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/id/kilocode.json b/webview-ui/src/i18n/locales/id/kilocode.json index 23436818e39..5e90fe0bf92 100644 --- a/webview-ui/src/i18n/locales/id/kilocode.json +++ b/webview-ui/src/i18n/locales/id/kilocode.json @@ -225,7 +225,11 @@ "noModelConfigured": "Tidak ditemukan model autocomplete yang sesuai. Silakan konfigurasi penyedia di pengaturan API.", "configureAutocompleteProfile": "Gunakan model apa pun dengan pergi ke profil dan mengonfigurasi Profil dengan Tipe Profil Autocomplete.", "model": "Model", - "provider": "Penyedia" + "provider": "Penyedia", + "enableChatAutocomplete": { + "description": "Jika diaktifkan, Kilo Code akan menyarankan pelengkapan saat Anda mengetik di kotak obrolan. Tekan Tab untuk menerima saran.", + "label": "Pelengkapan Otomatis Input Obrolan" + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/it/kilocode.json b/webview-ui/src/i18n/locales/it/kilocode.json index 866e8025709..162349e625a 100644 --- a/webview-ui/src/i18n/locales/it/kilocode.json +++ b/webview-ui/src/i18n/locales/it/kilocode.json @@ -232,7 +232,11 @@ "noModelConfigured": "Nessun modello di autocompletamento adatto trovato. Configura un provider nelle impostazioni API.", "configureAutocompleteProfile": "Usa qualsiasi modello andando nei profili e configurando un Profilo del Tipo di Profilo Autocompletamento.", "model": "Modello", - "provider": "Provider" + "provider": "Provider", + "enableChatAutocomplete": { + "label": "Completamento Automatico dell'Input della Chat", + "description": "Quando abilitato, Kilo Code suggerirà completamenti mentre digiti nell'area di chat. Premi Tab per accettare i suggerimenti." + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/ja/kilocode.json b/webview-ui/src/i18n/locales/ja/kilocode.json index 58c0b6a2986..94882932c74 100644 --- a/webview-ui/src/i18n/locales/ja/kilocode.json +++ b/webview-ui/src/i18n/locales/ja/kilocode.json @@ -239,7 +239,11 @@ "noModelConfigured": "適切なオートコンプリートモデルが見つかりませんでした。API設定でプロバイダーを設定してください。", "configureAutocompleteProfile": "プロファイルに移動し、プロファイルタイプがオートコンプリートのプロファイルを設定することで、任意のモデルを使用できます。", "model": "モデル", - "provider": "プロバイダー" + "provider": "プロバイダー", + "enableChatAutocomplete": { + "description": "有効にすると、Kilo Codeはチャット入力の入力中に候補を提案します。Tabキーを押して候補を受け入れることができます。", + "label": "チャット入力オートコンプリート" + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/ko/kilocode.json b/webview-ui/src/i18n/locales/ko/kilocode.json index c7b09556035..13f08147335 100644 --- a/webview-ui/src/i18n/locales/ko/kilocode.json +++ b/webview-ui/src/i18n/locales/ko/kilocode.json @@ -239,7 +239,11 @@ "noModelConfigured": "적합한 자동완성 모델을 찾을 수 없습니다. API 설정에서 제공자를 구성하세요.", "configureAutocompleteProfile": "프로필로 이동하여 프로필 유형이 자동완성인 프로필을 구성하면 모든 모델을 사용할 수 있습니다.", "model": "모델", - "provider": "제공자" + "provider": "제공자", + "enableChatAutocomplete": { + "label": "채팅 입력 자동완성", + "description": "활성화되면, Kilo Code는 채팅 입력 시 입력하는 대로 완성 제안을 제공합니다. Tab 키를 눌러 제안을 수락하세요." + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/nl/kilocode.json b/webview-ui/src/i18n/locales/nl/kilocode.json index 2b56cbfb172..edca73de3bf 100644 --- a/webview-ui/src/i18n/locales/nl/kilocode.json +++ b/webview-ui/src/i18n/locales/nl/kilocode.json @@ -239,7 +239,11 @@ "noModelConfigured": "Geen geschikt autocomplete-model gevonden. Configureer een provider in de API-instellingen.", "configureAutocompleteProfile": "Gebruik elk model door naar profielen te gaan en een Profiel van het Profieltype Autocomplete te configureren.", "model": "Model", - "provider": "Provider" + "provider": "Provider", + "enableChatAutocomplete": { + "label": "Chatinvoer Automatisch Aanvullen", + "description": "Wanneer ingeschakeld, zal Kilo Code suggesties voor aanvullingen geven terwijl je typt in de chatinvoer. Druk op Tab om suggesties te accepteren." + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/pl/kilocode.json b/webview-ui/src/i18n/locales/pl/kilocode.json index fdf2fb59a41..52c601fa0da 100644 --- a/webview-ui/src/i18n/locales/pl/kilocode.json +++ b/webview-ui/src/i18n/locales/pl/kilocode.json @@ -232,7 +232,11 @@ "noModelConfigured": "Nie znaleziono odpowiedniego modelu autouzupełniania. Skonfiguruj dostawcę w ustawieniach API.", "configureAutocompleteProfile": "Użyj dowolnego modelu, przechodząc do profili i konfigurując Profil typu Autouzupełnianie.", "model": "Model", - "provider": "Dostawca" + "provider": "Dostawca", + "enableChatAutocomplete": { + "label": "Automatyczne uzupełnianie wpisów czatu", + "description": "Po włączeniu, Kilo Code będzie sugerować uzupełnienia podczas pisania w oknie czatu. Naciśnij Tab, aby zaakceptować sugestie." + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/pt-BR/kilocode.json b/webview-ui/src/i18n/locales/pt-BR/kilocode.json index 8a6573bca14..73f3be5427b 100644 --- a/webview-ui/src/i18n/locales/pt-BR/kilocode.json +++ b/webview-ui/src/i18n/locales/pt-BR/kilocode.json @@ -232,7 +232,11 @@ "noModelConfigured": "Nenhum modelo de autocompletar adequado encontrado. Configure um provedor nas configurações da API.", "configureAutocompleteProfile": "Use qualquer modelo indo para perfis e configurando um Perfil do Tipo de Perfil Autocompletar.", "model": "Modelo", - "provider": "Provedor" + "provider": "Provedor", + "enableChatAutocomplete": { + "label": "Preenchimento Automático do Chat", + "description": "Quando ativado, o Kilo Code irá sugerir conclusões enquanto você digita no campo de chat. Pressione Tab para aceitar as sugestões." + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/ru/kilocode.json b/webview-ui/src/i18n/locales/ru/kilocode.json index 7b2fb32e176..314596d60af 100644 --- a/webview-ui/src/i18n/locales/ru/kilocode.json +++ b/webview-ui/src/i18n/locales/ru/kilocode.json @@ -232,7 +232,11 @@ "noModelConfigured": "Подходящая модель автодополнения не найдена. Настрой провайдера в настройках API.", "configureAutocompleteProfile": "Используй любую модель, перейдя в профили и настроив Профиль типа Автодополнение.", "model": "Модель", - "provider": "Провайдер" + "provider": "Провайдер", + "enableChatAutocomplete": { + "label": "Автодополнение в чате", + "description": "При включении Kilo Code будет предлагать варианты завершения во время ввода в окне чата. Нажмите Tab, чтобы принять предложения." + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/th/kilocode.json b/webview-ui/src/i18n/locales/th/kilocode.json index 81cd48263fc..f7c505ba6f2 100644 --- a/webview-ui/src/i18n/locales/th/kilocode.json +++ b/webview-ui/src/i18n/locales/th/kilocode.json @@ -239,7 +239,11 @@ "noModelConfigured": "ไม่พบโมเดลเติมข้อความอัตโนมัติที่เหมาะสม กรุณาตั้งค่าผู้ให้บริการในการตั้งค่า API", "configureAutocompleteProfile": "ใช้โมเดลใดก็ได้โดยไปที่โปรไฟล์และตั้งค่าโปรไฟล์ประเภทเติมข้อความอัตโนมัติ", "model": "โมเดล", - "provider": "ผู้ให้บริการ" + "provider": "ผู้ให้บริการ", + "enableChatAutocomplete": { + "label": "การเติมข้อความอัตโนมัติในการแชท", + "description": "เมื่อเปิดใช้งาน Kilo Code จะแนะนำข้อความเพื่อให้คุณพิมพ์ต่อในช่องแชท กดแท็บเพื่อยอมรับคำแนะนำ" + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/tr/kilocode.json b/webview-ui/src/i18n/locales/tr/kilocode.json index 8af77a0bcd7..c02004004cf 100644 --- a/webview-ui/src/i18n/locales/tr/kilocode.json +++ b/webview-ui/src/i18n/locales/tr/kilocode.json @@ -232,7 +232,11 @@ "noModelConfigured": "Uygun otomatik tamamlama modeli bulunamadı. Lütfen API ayarlarında bir sağlayıcı yapılandır.", "configureAutocompleteProfile": "Profillere giderek ve Otomatik Tamamlama Profil Türünde bir Profil yapılandırarak herhangi bir model kullan.", "model": "Model", - "provider": "Sağlayıcı" + "provider": "Sağlayıcı", + "enableChatAutocomplete": { + "label": "Sohbet Girişi Otomatik Tamamlama", + "description": "Etkinleştirildiğinde, Kilo Code sohbet girişinde yazarken tamamlama önerileri sunacaktır. Önerileri kabul etmek için Tab tuşuna basın." + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/uk/kilocode.json b/webview-ui/src/i18n/locales/uk/kilocode.json index 29531c43cd5..03c27989a17 100644 --- a/webview-ui/src/i18n/locales/uk/kilocode.json +++ b/webview-ui/src/i18n/locales/uk/kilocode.json @@ -239,7 +239,11 @@ "noModelConfigured": "Не знайдено відповідної моделі автодоповнення. Налаштуй провайдера в налаштуваннях API.", "configureAutocompleteProfile": "Використовуй будь-яку модель, перейшовши до профілів і налаштувавши Профіль типу Автодоповнення.", "model": "Модель", - "provider": "Провайдер" + "provider": "Провайдер", + "enableChatAutocomplete": { + "label": "Автозаповнення вводу чату", + "description": "Якщо ввімкнено, Kilo Code пропонуватиме доповнення під час введення в чаті. Натисніть Tab, щоб прийняти пропозиції." + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/vi/kilocode.json b/webview-ui/src/i18n/locales/vi/kilocode.json index 077736f568d..a95e83f25af 100644 --- a/webview-ui/src/i18n/locales/vi/kilocode.json +++ b/webview-ui/src/i18n/locales/vi/kilocode.json @@ -232,7 +232,11 @@ "noModelConfigured": "Không tìm thấy mô hình tự động hoàn thành phù hợp. Vui lòng cấu hình nhà cung cấp trong cài đặt API.", "configureAutocompleteProfile": "Sử dụng bất kỳ mô hình nào bằng cách vào hồ sơ và cấu hình Hồ sơ có Loại Hồ sơ là Tự động hoàn thành.", "model": "Mô hình", - "provider": "Nhà cung cấp" + "provider": "Nhà cung cấp", + "enableChatAutocomplete": { + "label": "Gợi ý Tự động Hoàn thành Trong Trò chuyện", + "description": "Khi được bật, Kilo Code sẽ đề xuất các gợi ý hoàn thành khi bạn nhập trong ô chat. Nhấn Tab để chấp nhận các gợi ý." + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/zh-CN/kilocode.json b/webview-ui/src/i18n/locales/zh-CN/kilocode.json index fc96b541b30..30e1725158e 100644 --- a/webview-ui/src/i18n/locales/zh-CN/kilocode.json +++ b/webview-ui/src/i18n/locales/zh-CN/kilocode.json @@ -239,7 +239,11 @@ "noModelConfigured": "未找到合适的自动补全模型。请在 API 设置中配置提供商。", "configureAutocompleteProfile": "前往配置文件并配置自动补全类型的配置文件即可使用任意模型。", "model": "模型", - "provider": "提供商" + "provider": "提供商", + "enableChatAutocomplete": { + "label": "聊天输入自动完成", + "description": "启用后,Kilo Code 将在您输入聊天内容时提供补全建议。按 Tab 键接受建议。" + } } }, "virtualProvider": { diff --git a/webview-ui/src/i18n/locales/zh-TW/kilocode.json b/webview-ui/src/i18n/locales/zh-TW/kilocode.json index 3a967a3e7fd..995f1b2818f 100644 --- a/webview-ui/src/i18n/locales/zh-TW/kilocode.json +++ b/webview-ui/src/i18n/locales/zh-TW/kilocode.json @@ -234,7 +234,11 @@ "noModelConfigured": "找不到合適的自動補全模型。請在 API 設定中配置提供者。", "configureAutocompleteProfile": "前往設定檔並設定自動補全類型的設定檔即可使用任意模型。", "model": "模型", - "provider": "供應商" + "provider": "供應商", + "enableChatAutocomplete": { + "label": "聊天输入自动完成", + "description": "启用后,Kilo Code 将在您在聊天输入框中输入时提供补全建议。按 Tab 键接受建议。" + } } }, "virtualProvider": { diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index e0f74c9c5de..581001cda75 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -410,6 +410,15 @@ vscode-dropdown::part(listbox) { font-family: var(--font-mono); } +/* kilocode_change start: FIM autocomplete ghost text */ +.chat-ghost-text { + color: var(--vscode-editorGhostText-foreground, rgba(255, 255, 255, 0.4)); + opacity: 0.6; + pointer-events: none; + user-select: none; +} +/* kilocode_change end: FIM autocomplete ghost text */ + /** * vscrui Overrides / Hacks */ diff --git a/webview-ui/src/utils/highlight.ts b/webview-ui/src/utils/highlight.ts index 21c84f993a8..02117e22555 100644 --- a/webview-ui/src/utils/highlight.ts +++ b/webview-ui/src/utils/highlight.ts @@ -3,7 +3,7 @@ import { LRUCache } from "lru-cache" // LRU cache for escapeHtml with reasonable size limit const escapeHtmlCache = new LRUCache({ max: 500 }) -function escapeHtml(text: string): string { +export function escapeHtml(text: string): string { // Check cache first const cached = escapeHtmlCache.get(text) if (cached !== undefined) {