Merge pull request #8654 from Kilo-Org/session/agent_54a63e1a-9980-4a7d-b2d4-13233973f500

Clean up dead autocomplete code
This commit is contained in:
Mark IJbema
2026-04-09 12:40:05 +02:00
committed by GitHub
129 changed files with 33 additions and 14011 deletions
-1
View File
@@ -12,7 +12,6 @@
"script/*.ts"
],
"project": ["src/**/*.ts", "webview-ui/**/*.{ts,tsx}"],
"ignore": ["src/services/autocomplete/**"],
"ignoreExportsUsedInFile": true,
"exclude": ["dependencies", "devDependencies", "optionalPeerDependencies", "unlisted", "unresolved", "binaries"]
}
@@ -1,15 +1,24 @@
import * as vscode from "vscode"
import { AutocompleteModel } from "../AutocompleteModel"
import { AutocompleteContext, VisibleCodeContext } from "../types"
import type { AutocompleteContext, VisibleCodeContext } from "../types"
import { removePrefixOverlap } from "../continuedev/core/autocomplete/postprocessing/removePrefixOverlap.js"
import { AutocompleteTelemetry } from "../classic-auto-complete/AutocompleteTelemetry"
import { postprocessAutocompleteSuggestion } from "../classic-auto-complete/uselessSuggestionFilter"
import { VisibleCodeTracker } from "../context/VisibleCodeTracker"
import { FileIgnoreController } from "../shims/FileIgnoreController"
import type { KiloConnectionService } from "../../cli-backend"
import type { ChatCompletionRequestMessage, ChatCompletionResponseSender } from "./handleChatCompletionRequest"
import { finalizeChatSuggestion, buildChatPrefix } from "./chat-autocomplete-utils"
interface ChatCompletionRequestMessage {
type: "requestChatCompletion"
text: string
requestId: string
}
interface ChatCompletionResponseSender {
postMessage(message: { type: "chatCompletionResult"; text: string; requestId: string }): void
}
/**
* Chat textarea autocomplete with cached per-request objects.
*
@@ -1,29 +0,0 @@
import { AutocompleteTelemetry } from "../classic-auto-complete/AutocompleteTelemetry"
export interface ChatCompletionAcceptedMessage {
type: "chatCompletionAccepted"
suggestionLength?: number
}
// 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: ChatCompletionAcceptedMessage): void {
const telemetry = getChatAutocompleteTelemetry()
telemetry.captureAcceptSuggestion(message.suggestionLength)
}
@@ -1,43 +0,0 @@
import * as vscode from "vscode"
import { VisibleCodeTracker } from "../context/VisibleCodeTracker"
import { FileIgnoreController } from "../shims/FileIgnoreController"
import { ChatTextAreaAutocomplete } from "./ChatTextAreaAutocomplete"
import type { KiloConnectionService } from "../../cli-backend"
export interface ChatCompletionRequestMessage {
type: "requestChatCompletion"
text?: string
requestId?: string
}
export interface ChatCompletionResponseSender {
postMessage(message: { type: "chatCompletionResult"; text: string; requestId: string }): void
}
/**
* Handles a chat completion request from the webview.
* Captures visible code context and generates an autocomplete suggestion.
*/
export async function handleChatCompletionRequest(
message: ChatCompletionRequestMessage,
responseSender: ChatCompletionResponseSender,
connectionService: KiloConnectionService,
): Promise<void> {
const userText = message.text || ""
const requestId = message.requestId || ""
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ""
const ignoreController = new FileIgnoreController(workspacePath)
await ignoreController.initialize()
const tracker = new VisibleCodeTracker(workspacePath, ignoreController)
const visibleContext = await tracker.captureVisibleCode()
const autocomplete = new ChatTextAreaAutocomplete(connectionService)
const { suggestion } = await autocomplete.getCompletion(userText, visibleContext)
responseSender.postMessage({ type: "chatCompletionResult", text: suggestion, requestId })
ignoreController.dispose()
}
@@ -1,807 +0,0 @@
# API Reference
Complete API documentation for the Autocomplete & NextEdit library.
## Table of Contents
- [CompletionProvider](#completionprovider)
- [NextEditProvider](#nexteditprovider)
- [MinimalConfigProvider](#minimalconfigprovider)
- [Core Interfaces](#core-interfaces)
- [LLM Adapters](#llm-adapters)
- [Types and Interfaces](#types-and-interfaces)
---
## CompletionProvider
The main class for providing AI-powered code autocompletion.
**Location**: [`core/autocomplete/CompletionProvider.ts`](core/autocomplete/CompletionProvider.ts)
### Constructor
```typescript
constructor(
configHandler: MinimalConfigProvider,
ide: IDE,
_injectedGetLlm: () => Promise<ILLM | undefined>,
_onError: (e: any) => void,
getDefinitionsFromLsp: GetLspDefinitionsFunction
)
```
**Parameters**:
- `configHandler`: Configuration provider for autocomplete options
- `ide`: IDE interface implementation for file I/O and editor operations
- `_injectedGetLlm`: Async function that returns the LLM to use for completions
- `_onError`: Error callback function for handling autocomplete errors
- `getDefinitionsFromLsp`: Function to retrieve LSP definitions for enhanced context
### Methods
#### `provideInlineCompletionItems()`
Generates an autocomplete completion for the given input.
```typescript
async provideInlineCompletionItems(
input: AutocompleteInput,
token: AbortSignal | undefined,
force?: boolean
): Promise<AutocompleteOutcome | undefined>
```
**Parameters**:
- `input`: Autocomplete context including file path, cursor position, recent edits
- `token`: AbortSignal to cancel the request
- `force`: If true, bypasses debouncing
**Returns**: `AutocompleteOutcome` containing the completion text and metadata, or `undefined` if no completion
**Example**:
```typescript
const outcome = await completionProvider.provideInlineCompletionItems(
{
filepath: "/path/to/file.ts",
pos: { line: 10, character: 5 },
completionId: "unique-completion-id",
recentlyEditedRanges: [],
recentlyEditedFiles: new Map(),
clipboardText: "",
},
abortController.signal,
)
```
#### `accept()`
Marks a completion as accepted by the user.
```typescript
accept(completionId: string): void
```
**Parameters**:
- `completionId`: Unique identifier for the accepted completion
**Side Effects**: Updates bracket matching service and completion cache
#### `markDisplayed()`
Marks a completion as having been displayed to the user.
```typescript
markDisplayed(completionId: string, outcome: AutocompleteOutcome): void
```
**Parameters**:
- `completionId`: Unique identifier for the completion
- `outcome`: The autocomplete outcome that was displayed
#### `cancel()`
Cancels any in-progress autocomplete requests.
```typescript
cancel(): void
```
### Configuration Options
See [`MinimalConfigProvider`](#minimalconfigprovider) for configuration options.
---
## NextEditProvider
The main class for providing predictive multi-location code edits.
**Location**: [`core/nextEdit/NextEditProvider.ts`](core/nextEdit/NextEditProvider.ts)
### Constructor
```typescript
constructor(
configHandler: MinimalConfigProvider,
ide: IDE,
_injectedGetLlm: () => Promise<ILLM | undefined>,
_onError: (e: any) => void
)
```
**Parameters**:
- `configHandler`: Configuration provider
- `ide`: IDE interface implementation
- `_injectedGetLlm`: Function returning the LLM for edit predictions
- `_onError`: Error callback
### Methods
#### `getNextEditPrediction()`
Generates predicted edits based on context and recent changes.
```typescript
async getNextEditPrediction(
context: ModelSpecificContext,
signal?: AbortSignal,
usingFullFileDiff?: boolean
): Promise<NextEditOutcome | undefined>
```
**Parameters**:
- `context`: Context including file contents, cursor position, recent edits
- `signal`: Optional AbortSignal to cancel the request
- `usingFullFileDiff`: If true, generates full-file diffs; if false, only edits within a region
**Returns**: `NextEditOutcome` containing predicted edits and final cursor position
**Example**:
```typescript
const outcome = await nextEditProvider.getNextEditPrediction(
{
filepath: "/path/to/file.ts",
pos: { line: 15, character: 0 },
fileContents: currentFileContents,
userEdits: recentDiff,
// ... other context
},
abortController.signal,
false, // Use partial file diff
)
if (outcome) {
console.log("Edit regions:", outcome.editableRegions)
console.log("Diff lines:", outcome.diffLines)
console.log("New cursor:", outcome.finalCursorPosition)
}
```
---
## MinimalConfigProvider
Simple configuration provider that replaces the complex Continue config system.
**Location**: [`core/autocomplete/MinimalConfig.ts`](core/autocomplete/MinimalConfig.ts)
### Constructor
```typescript
constructor(config?: Partial<MinimalConfig>)
```
**Parameters**:
- `config`: Optional partial configuration to override defaults
**Example**:
```typescript
const configProvider = new MinimalConfigProvider({
tabAutocompleteOptions: {
debounceDelay: 200,
maxPromptTokens: 2048,
prefixPercentage: 0.5,
suffixPercentage: 0.3,
useCache: true,
onlyMyCode: false,
},
experimental: {
enableStaticContextualization: true,
},
})
```
### Methods
#### `loadConfig()`
Returns the configuration object.
```typescript
async loadConfig(): Promise<{ config: MinimalConfig }>
```
**Returns**: Promise resolving to an object containing the config
#### `getAutocompleteOptions()`
Gets autocomplete-specific options.
```typescript
getAutocompleteOptions(): TabAutocompleteOptions
```
**Returns**: Autocomplete configuration options
#### `isStaticContextualizationEnabled()`
Checks if static contextualization is enabled.
```typescript
isStaticContextualizationEnabled(): boolean
```
**Returns**: True if enabled, false otherwise
### Configuration Interface
```typescript
interface MinimalConfig {
tabAutocompleteOptions?: TabAutocompleteOptions
experimental?: {
enableStaticContextualization?: boolean
}
modelsByRole?: {
autocomplete?: ILLM[]
}
selectedModelByRole?: {
autocomplete?: ILLM
}
}
```
### TabAutocompleteOptions
```typescript
interface TabAutocompleteOptions {
debounceDelay?: number // Debounce delay in ms (default: 150)
maxPromptTokens?: number // Max tokens for prompt (default: 1024)
prefixPercentage?: number // Percentage of tokens for prefix (default: 0.5)
suffixPercentage?: number // Percentage of tokens for suffix (default: 0.3)
maxSuffixPercentage?: number // Max suffix tokens percentage (default: 0.5)
useCache?: boolean // Enable completion caching (default: true)
onlyMyCode?: boolean // Only use workspace files for context (default: false)
template?: string // Custom prompt template
useFileSuffix?: boolean // Include file suffix in context (default: true)
multilineCompletions?: "always" | "never" | "auto" // Multiline behavior (default: 'auto')
slidingWindowPrefixPercentage?: number // Sliding window prefix % (default: 0.75)
slidingWindowSize?: number // Sliding window size (default: 500)
maxSnippetPercentage?: number // Max tokens for snippets (default: 0.6)
recentlyEditedSimilarityThreshold?: number // Similarity threshold (default: 0.3)
useOtherFiles?: boolean // Use other files for context (default: true)
disableInFiles?: string[] // Glob patterns to disable autocomplete
stopTokens?: string[] // Custom stop tokens
tokensPerCompletion?: number // Tokens per completion (default: 256)
transform?: boolean // Apply post-processing transforms (default: true)
}
```
---
## Core Interfaces
### IDE Interface
The IDE interface abstracts editor operations. Implement this to integrate with your editor.
**Location**: [`core/index.d.ts`](core/index.d.ts)
```typescript
interface IDE {
// File Operations
readFile(filepath: string): Promise<string>
writeFile(filepath: string, contents: string): Promise<void>
// Workspace
getWorkspaceDirs(): Promise<string[]>
listWorkspaceContents(directory?: string): Promise<string[]>
// Editor State
getCurrentFile(): Promise<FileWithContents | undefined>
getCursorPosition(): Promise<Position>
getVisibleFiles(): Promise<string[]>
// Code Navigation
getDefinition(filepath: string, position: Position): Promise<Location[]>
getReferences(filepath: string, position: Position): Promise<Location[]>
getSymbols(filepath: string): Promise<SymbolWithRange[]>
// File Information
readRangeInFile(filepath: string, range: Range): Promise<string>
getStats(filepath: string): Promise<FileStats>
// Edits
applyEdits(edits: FileEdit[]): Promise<void>
// Diff/SCM
getDiff(includeUnstaged: boolean): Promise<string>
getRepoName(dir: string): Promise<string | undefined>
getBranch(dir: string): Promise<string>
// UI
showMessage(message: string, severity?: "info" | "warning" | "error"): Promise<void>
showToast(type: "info" | "warning" | "error", message: string, ...actions: string[]): Promise<string | undefined>
// Terminal
runCommand(command: string, options?: TerminalOptions): Promise<string>
// Clipboard
getClipboardContent(): Promise<{ text: string; copiedAt: number } | undefined>
// Search
getSearchResults(query: string): Promise<string>
subprocess(command: string, cwd?: string): Promise<[string, string]>
// Other
getIdeInfo(): Promise<IdeInfo>
getIdeSettings(): Promise<IdeSettings>
isTelemetryEnabled(): Promise<boolean>
getUniqueId(): Promise<string>
}
```
**Key Methods to Implement**:
- `readFile()`, `writeFile()`: Essential for file I/O
- `getWorkspaceDirs()`: Returns workspace root directories
- `getCurrentFile()`, `getCursorPosition()`: Current editor state
- `applyEdits()`: Apply code changes
- `getDefinition()`: LSP-like functionality for context gathering
### ILLM Interface
The ILLM (Language Model) interface abstracts LLM providers.
**Location**: [`core/index.d.ts`](core/index.d.ts)
```typescript
interface ILLM {
// Required properties
uniqueId: string
model: string
contextLength: number
completionOptions: CompletionOptions
// Provider info
get providerName(): string
get underlyingProviderName(): string
// Optional
apiKey?: string
apiBase?: string
autocompleteOptions?: Partial<TabAutocompleteOptions>
promptTemplates?: PromptTemplates
// Completion methods
complete(prompt: string, signal: AbortSignal, options?: LLMFullCompletionOptions): Promise<string>
streamComplete(
prompt: string,
signal: AbortSignal,
options?: LLMFullCompletionOptions,
): AsyncGenerator<string, PromptLog>
streamFim(
prefix: string,
suffix: string,
signal: AbortSignal,
options?: LLMFullCompletionOptions,
): AsyncGenerator<string, PromptLog>
// Chat methods
chat(messages: ChatMessage[], signal: AbortSignal, options?: LLMFullCompletionOptions): Promise<ChatMessage>
streamChat(
messages: ChatMessage[],
signal: AbortSignal,
options?: LLMFullCompletionOptions,
): AsyncGenerator<ChatMessage, PromptLog>
// Utility methods
countTokens(text: string): number
supportsImages(): boolean
supportsCompletions(): boolean
supportsFim(): boolean
listModels(): Promise<string[]>
}
```
---
## LLM Adapters
### OpenAI
Pre-built adapter for OpenAI and OpenAI-compatible APIs.
**Location**: [`core/llm/llms/OpenAI.ts`](core/llm/llms/OpenAI.ts)
```typescript
import OpenAI from "@continuedev/core/llm/llms/OpenAI"
const llm = new OpenAI({
model: "gpt-4",
apiKey: process.env.OPENAI_API_KEY,
apiBase: "https://api.openai.com/v1", // Optional custom base URL
completionOptions: {
temperature: 0.1,
maxTokens: 1000,
},
})
```
**Constructor Options**:
```typescript
interface OpenAIOptions {
model: string
apiKey: string
apiBase?: string
completionOptions?: CompletionOptions
contextLength?: number
autocompleteOptions?: Partial<TabAutocompleteOptions>
}
```
### Creating Custom LLM Adapters
To create a custom LLM adapter, implement the `ILLM` interface:
```typescript
import { ILLM, CompletionOptions } from "@continuedev/core"
class CustomLLM implements ILLM {
uniqueId = "custom-llm"
model: string
contextLength: number
completionOptions: CompletionOptions
get providerName() {
return "custom"
}
get underlyingProviderName() {
return "custom"
}
constructor(options: { model: string }) {
this.model = options.model
this.contextLength = 4096
this.completionOptions = {
model: options.model,
temperature: 0.1,
}
}
async complete(prompt: string, signal: AbortSignal): Promise<string> {
// Call your LLM API
const response = await fetch("your-api-endpoint", {
method: "POST",
body: JSON.stringify({ prompt }),
signal,
})
return await response.text()
}
async *streamComplete(prompt: string, signal: AbortSignal) {
// Stream from your LLM API
for await (const chunk of streamFromAPI(prompt, signal)) {
yield chunk
}
return { modelTitle: this.model, prompt, completion: "" }
}
// Implement other required methods...
countTokens(text: string): number {
return text.length / 4 // Rough estimate
}
supportsImages() {
return false
}
supportsCompletions() {
return true
}
supportsFim() {
return false
}
// ... other methods
}
```
---
## Types and Interfaces
### AutocompleteInput
Input for autocomplete requests.
```typescript
interface AutocompleteInput {
filepath: string // Path to the file being edited
pos: Position // Cursor position
completionId: string // Unique ID for this completion request
recentlyEditedRanges: Range[] // Recently edited ranges in this file
recentlyEditedFiles: Map<string, [Range, number][]> // Recently edited files
clipboardText: string // Current clipboard content
manuallyPassFileContext?: RangeInFile[] // Manually provided context
}
```
### AutocompleteOutcome
Result of an autocomplete request.
```typescript
interface AutocompleteOutcome {
completion: string // The completion text
completionId: string // Unique ID for this completion
filepath: string // File path
prefix: string // Code prefix (before cursor)
suffix: string // Code suffix (after cursor)
prompt: string // Full prompt sent to LLM
modelTitle: string // Model used
modelProvider: string // Provider used
completionOptions: CompletionOptions // Options used
cacheHit: boolean // Whether cached
latency: number // Response latency in ms
}
```
### NextEditOutcome
Result of a NextEdit prediction.
```typescript
interface NextEditOutcome {
edits: string // The predicted edit text
diffLines: DiffLine[] // Diff representation
editableRegions: Range[] // Regions that were edited
finalCursorPosition: Position // Predicted cursor position after edits
filepath: string // File path
prompt: string // Prompt sent to LLM
modelTitle: string // Model used
latency: number // Response latency in ms
}
```
### Position
Position in a text document.
```typescript
interface Position {
line: number // 0-based line number
character: number // 0-based character offset
}
```
### Range
Range in a text document.
```typescript
interface Range {
start: Position // Start position (inclusive)
end: Position // End position (exclusive)
}
```
### RangeInFile
Range in a specific file.
```typescript
interface RangeInFile {
filepath: string // File path
range: Range // Range within the file
}
```
### DiffLine
A single line in a diff.
```typescript
interface DiffLine {
type: "same" | "new" | "old" // Type of change
line: string // Line content
lineNumber: number // Line number in file
}
```
### FileEdit
An edit to apply to a file.
```typescript
interface FileEdit {
filepath: string // File to edit
range: Range // Range to replace
replacement: string // New content
}
```
---
## Usage Examples
### Complete Example: Autocomplete with Custom IDE
```typescript
import { CompletionProvider, MinimalConfigProvider } from "@continuedev/core/autocomplete"
import { IDE, ILLM, Position } from "@continuedev/core"
import OpenAI from "@continuedev/core/llm/llms/OpenAI"
// 1. Implement IDE interface
class MyIDE implements IDE {
async readFile(filepath: string): Promise<string> {
return fs.readFileSync(filepath, "utf-8")
}
async getWorkspaceDirs(): Promise<string[]> {
return ["/path/to/workspace"]
}
async getCurrentFile() {
return {
filepath: this.currentFilePath,
contents: await this.readFile(this.currentFilePath),
}
}
async getCursorPosition(): Promise<Position> {
return this.cursorPosition
}
// ... implement other methods
}
// 2. Set up configuration
const config = new MinimalConfigProvider({
tabAutocompleteOptions: {
debounceDelay: 150,
maxPromptTokens: 1024,
useCache: true,
},
})
// 3. Set up LLM
const getLlm = async (): Promise<ILLM> => {
return new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
model: "gpt-4",
})
}
// 4. Create completion provider
const ide = new MyIDE()
const provider = new CompletionProvider(
config,
ide,
getLlm,
(error) => console.error(error),
async () => [], // LSP definitions function
)
// 5. Request completion
const outcome = await provider.provideInlineCompletionItems(
{
filepath: "/path/to/file.ts",
pos: { line: 10, character: 5 },
completionId: "completion-1",
recentlyEditedRanges: [],
recentlyEditedFiles: new Map(),
clipboardText: "",
},
new AbortController().signal,
)
if (outcome) {
console.log("Completion:", outcome.completion)
provider.markDisplayed("completion-1", outcome)
}
```
---
## Error Handling
Both `CompletionProvider` and `NextEditProvider` accept an error callback:
```typescript
const onError = (error: any) => {
if (error instanceof Error) {
console.error("Error:", error.message)
// Show user notification
showNotification(error.message)
}
}
const provider = new CompletionProvider(
config,
ide,
getLlm,
onError, // Error callback
getLspDefinitions,
)
```
Common errors:
- **LLM API errors**: Network failures, invalid API keys, rate limits
- **File I/O errors**: Missing files, permission errors
- **Timeout errors**: Long-running requests that are aborted
---
## Performance Considerations
### Caching
Completions are automatically cached using an LRU cache. Configure caching:
```typescript
const config = new MinimalConfigProvider({
tabAutocompleteOptions: {
useCache: true, // Enable caching (default)
},
})
```
### Debouncing
Prevent excessive LLM calls during rapid typing:
```typescript
const config = new MinimalConfigProvider({
tabAutocompleteOptions: {
debounceDelay: 150, // Wait 150ms before requesting (default)
},
})
```
### Abort Signals
Always provide AbortSignals to cancel in-progress requests:
```typescript
const controller = new AbortController()
// Start request
const promise = provider.provideInlineCompletionItems(input, controller.signal)
// Cancel if needed
controller.abort()
```
---
## See Also
- [README.md](README.md) - Project overview and quick start
- [ARCHITECTURE.md](ARCHITECTURE.md) - Technical architecture details
- [EXAMPLES.md](EXAMPLES.md) - More usage examples
- [Core TypeScript Definitions](core/index.d.ts) - Full type definitions
@@ -1,272 +0,0 @@
import { MinimalConfigProvider } from "./MinimalConfig.js"
import { IDE, ILLM } from "../index.js"
import { DEFAULT_AUTOCOMPLETE_OPTS } from "../util/parameters.js"
import { shouldCompleteMultiline } from "./classification/shouldCompleteMultiline.js"
import { ContextRetrievalService } from "./context/ContextRetrievalService.js"
import { isSecurityConcern } from "../indexing/ignore.js"
import { BracketMatchingService } from "./filtering/BracketMatchingService.js"
import { CompletionStreamer } from "./generation/CompletionStreamer.js"
import { postprocessCompletion } from "./postprocessing/index.js"
import { shouldPrefilter } from "./prefiltering/index.js"
import { getAllSnippetsWithoutRace } from "./snippets/index.js"
import { renderPromptWithTokenLimit } from "./templating/index.js"
import { GetLspDefinitionsFunction } from "./types.js"
import { AutocompleteDebouncer } from "./util/AutocompleteDebouncer.js"
import { AutocompleteLoggingService } from "./util/AutocompleteLoggingService.js"
import { AutocompleteLruCacheInMem } from "./util/AutocompleteLruCacheInMem.js"
import { HelperVars } from "./util/HelperVars.js"
import { AutocompleteInput, AutocompleteOutcome } from "./util/types.js"
// Errors that can be expected on occasion even during normal functioning should not be shown.
// Not worth disrupting the user to tell them that a single autocomplete request didn't go through
const ERRORS_TO_IGNORE = [
// From Ollama
"unexpected server status",
"operation was aborted",
]
export class CompletionProvider {
private autocompleteCache = AutocompleteLruCacheInMem.get()
public errorsShown: Set<string> = new Set()
private bracketMatchingService = new BracketMatchingService()
private debouncer = new AutocompleteDebouncer()
private completionStreamer: CompletionStreamer
private loggingService = new AutocompleteLoggingService()
private contextRetrievalService: ContextRetrievalService
constructor(
private readonly configHandler: MinimalConfigProvider,
private readonly ide: IDE,
private readonly _injectedGetLlm: () => Promise<ILLM | undefined>,
private readonly _onError: (e: unknown) => void,
private readonly getDefinitionsFromLsp: GetLspDefinitionsFunction,
) {
this.completionStreamer = new CompletionStreamer(this.onError.bind(this))
this.contextRetrievalService = new ContextRetrievalService(this.ide)
}
private async _prepareLlm(): Promise<ILLM | undefined> {
const llm = await this._injectedGetLlm()
if (!llm) {
return undefined
}
// Temporary fix for JetBrains autocomplete bug as described in https://github.com/continuedev/continue/pull/3022
if (llm.model === undefined && llm.completionOptions?.model !== undefined) {
llm.model = llm.completionOptions.model
}
// Ignore empty API keys for Mistral since we currently write
// a template provider without one during onboarding
if (llm.providerName === "mistral" && llm.apiKey === "") {
return undefined
}
// Set temperature (but don't override)
if (llm.completionOptions.temperature === undefined) {
llm.completionOptions.temperature = 0.01
}
return llm
}
private onError(e: unknown) {
if (
ERRORS_TO_IGNORE.some((err) => (typeof e === "string" ? e.includes(err) : (e as Error)?.message?.includes(err)))
) {
return
}
console.warn("Error generating autocompletion: ", e)
const errorMessage = e instanceof Error ? e.message : String(e)
if (!this.errorsShown.has(errorMessage)) {
this.errorsShown.add(errorMessage)
this._onError(e)
}
}
public cancel() {
this.loggingService.cancel()
}
public accept(completionId: string) {
const outcome = this.loggingService.accept(completionId)
if (!outcome) {
return
}
this.bracketMatchingService.handleAcceptedCompletion(outcome.completion, outcome.filepath)
}
public markDisplayed(completionId: string, outcome: AutocompleteOutcome) {
this.loggingService.markDisplayed(completionId, outcome)
}
private async _getAutocompleteOptions(llm: ILLM) {
const { config } = await this.configHandler.loadConfig()
const options = {
...DEFAULT_AUTOCOMPLETE_OPTS,
...config?.tabAutocompleteOptions,
...llm.autocompleteOptions,
}
// Enable static contextualization if defined.
if (config?.experimental?.enableStaticContextualization) {
options.experimental_enableStaticContextualization = false
}
return options
}
public async provideInlineCompletionItems(
input: AutocompleteInput,
token: AbortSignal | undefined,
force?: boolean,
): Promise<AutocompleteOutcome | undefined> {
try {
// Create abort signal if not given
if (!token) {
const controller = this.loggingService.createAbortController(input.completionId)
token = controller.signal
}
const startTime = Date.now()
const llm = await this._prepareLlm()
if (!llm) {
return undefined
}
if (isSecurityConcern(input.filepath)) {
return undefined
}
const options = await this._getAutocompleteOptions(llm)
// Debounce
if (!force) {
if (await this.debouncer.delayAndShouldDebounce(options.debounceDelay)) {
return undefined
}
}
const helper = await HelperVars.create(input, options, llm.model, this.ide)
if (await shouldPrefilter(helper, await this.ide.getWorkspaceDirs())) {
return undefined
}
const [snippetPayload, workspaceDirs] = await Promise.all([
getAllSnippetsWithoutRace({
helper,
ide: this.ide,
getDefinitionsFromLsp: this.getDefinitionsFromLsp,
contextRetrievalService: this.contextRetrievalService,
}),
this.ide.getWorkspaceDirs(),
])
const { prompt, prefix, suffix, completionOptions } = renderPromptWithTokenLimit({
snippetPayload,
workspaceDirs,
helper,
llm,
})
// Completion
let completion: string | undefined = ""
const cache = await this.autocompleteCache
const cachedCompletion = helper.options.useCache ? await cache.get(helper.prunedPrefix) : undefined
let cacheHit = false
if (cachedCompletion) {
// Cache
cacheHit = true
completion = cachedCompletion
} else {
const multiline = !helper.options.transform || shouldCompleteMultiline(helper)
const completionStream = this.completionStreamer.streamCompletionWithFilters(
token,
llm,
prefix,
suffix,
prompt,
multiline,
completionOptions,
helper,
)
for await (const update of completionStream) {
completion += update
}
// Don't postprocess if aborted
if (token.aborted) {
return undefined
}
const processedCompletion = helper.options.transform
? postprocessCompletion({
completion,
prefix: helper.prunedPrefix,
suffix: helper.prunedSuffix,
llm,
})
: completion
completion = processedCompletion
}
if (!completion) {
return undefined
}
const outcome: AutocompleteOutcome = {
time: Date.now() - startTime,
completion,
prefix,
suffix,
prompt,
modelProvider: llm.underlyingProviderName,
modelName: llm.model,
completionOptions,
cacheHit,
filepath: helper.filepath,
numLines: completion.split("\n").length,
completionId: helper.input.completionId,
gitRepo: "fake-placeholder", //MINIMAL_REPO - came from git
uniqueId: await this.ide.getUniqueId(),
timestamp: new Date().toISOString(),
profileType: this.configHandler.currentProfile?.profileDescription.profileType,
...helper.options,
}
if (options.experimental_enableStaticContextualization) {
outcome.enabledStaticContextualization = true
}
//////////
// Save to cache
if (!outcome.cacheHit && helper.options.useCache) {
;(await this.autocompleteCache)
.put(outcome.prefix, outcome.completion)
.catch((e) => console.warn(`Failed to save to cache: ${e.message}`))
}
// When using the JetBrains extension, Mark as displayed
const ideType = (await this.ide.getIdeInfo()).ideType
if (ideType === "jetbrains") {
this.markDisplayed(input.completionId, outcome)
}
return outcome
} catch (e: unknown) {
this.onError(e)
return undefined
} finally {
this.loggingService.deleteAbortController(input.completionId)
}
}
}
export default CompletionProvider
@@ -1,126 +0,0 @@
/**
* Minimal configuration for autocomplete features.
* This replaces the complex ConfigHandler system with simple hardcoded defaults.
*
* Analysis of ConfigHandler usage:
* - CompletionProvider needs: config.tabAutocompleteOptions, config.experimental.enableStaticContextualization, currentProfile.profileType
*
* The profileType is only used for logging/telemetry, so we can set it to undefined for a minimal extraction.
*/
import { ILLM, TabAutocompleteOptions } from "../index.js"
import { DEFAULT_AUTOCOMPLETE_OPTS } from "../util/parameters.js"
interface MinimalConfig {
tabAutocompleteOptions?: TabAutocompleteOptions
experimental?: {
enableStaticContextualization?: boolean
}
// Minimal model selection support for NextEdit context fetching
modelsByRole?: {
autocomplete?: ILLM[]
}
selectedModelByRole?: {
autocomplete?: ILLM
edit?: ILLM
chat?: ILLM
rerank?: ILLM
}
rules?: unknown[]
}
interface MinimalProfile {
profileDescription: {
profileType?: "control-plane" | "local" | "platform"
}
}
/**
* Default configuration with hardcoded values suitable for autocomplete/NextEdit.
* Uses the same defaults from DEFAULT_AUTOCOMPLETE_OPTS.
*/
const DEFAULT_MINIMAL_CONFIG: MinimalConfig = {
tabAutocompleteOptions: {
...DEFAULT_AUTOCOMPLETE_OPTS,
},
experimental: {
enableStaticContextualization: false,
},
modelsByRole: {
autocomplete: [],
},
selectedModelByRole: {
autocomplete: undefined,
},
}
/**
* Simple config provider that replaces ConfigHandler for autocomplete/NextEdit.
* Returns hardcoded configuration without dependencies on control-plane.
*/
export class MinimalConfigProvider {
private config: MinimalConfig
public currentProfile: MinimalProfile | undefined
constructor(config?: Partial<MinimalConfig>) {
this.config = {
...DEFAULT_MINIMAL_CONFIG,
...config,
tabAutocompleteOptions: {
...DEFAULT_AUTOCOMPLETE_OPTS,
...config?.tabAutocompleteOptions,
} as TabAutocompleteOptions,
experimental: {
...DEFAULT_MINIMAL_CONFIG.experimental,
...config?.experimental,
},
}
// Set a minimal profile for logging purposes
// In a minimal extraction, we don't have a control-plane profile
this.currentProfile = undefined
}
/**
* Returns the config in the same shape as ConfigHandler.loadConfig()
* This maintains API compatibility with existing code.
*/
async loadConfig(): Promise<{ config: MinimalConfig }> {
return { config: this.config }
}
/**
* Get autocomplete options directly
*/
getAutocompleteOptions(): TabAutocompleteOptions {
return this.config.tabAutocompleteOptions || DEFAULT_AUTOCOMPLETE_OPTS
}
/**
* Check if static contextualization is enabled
*/
isStaticContextualizationEnabled(): boolean {
return this.config.experimental?.enableStaticContextualization ?? false
}
/**
* Reload config (stub for compatibility)
*/
async reloadConfig(..._args: unknown[]): Promise<void> {
// No-op for minimal config
}
/**
* Register config update handler (stub for compatibility)
*/
onConfigUpdate(_handler: (event: { config: MinimalConfig; configLoadInterrupted: boolean }) => void): void {
// No-op for minimal config
}
/**
* Register custom context provider (stub for compatibility)
*/
registerCustomContextProvider(_provider: unknown): void {
// No-op for minimal config
}
}
@@ -1,37 +0,0 @@
import { AutocompleteLanguageInfo } from "../constants/AutocompleteLanguageInfo"
import { HelperVars } from "../util/HelperVars"
function shouldCompleteMultilineBasedOnLanguage(language: AutocompleteLanguageInfo, prefix: string, suffix: string) {
return language.useMultiline?.({ prefix, suffix }) ?? true
}
export function shouldCompleteMultiline(helper: HelperVars) {
switch (helper.options.multilineCompletions) {
case "always":
return true
case "never":
return false
default:
break
}
// Always single-line if an intellisense option is selected
if (helper.input.selectedCompletionInfo) {
return true
}
// // Don't complete multi-line if you are mid-line
// if (isMidlineCompletion(helper.fullPrefix, helper.fullSuffix)) {
// return false;
// }
// Don't complete multi-line for single-line comments
if (
helper.lang.singleLineComment &&
helper.fullPrefix.split("\n").slice(-1)[0]?.trimStart().startsWith(helper.lang.singleLineComment)
) {
return false
}
return shouldCompleteMultilineBasedOnLanguage(helper.lang, helper.prunedPrefix, helper.prunedSuffix)
}
@@ -1,29 +0,0 @@
// @ts-nocheck
const getAddress = (person: Person): Address => {
// TODO
}
const logPerson = (person: Person) => {
// TODO
}
const getHardcodedAddress = (): Address => {
// TODO
}
const getAddresses = (people: Person[]): Address[] => {
// TODO
}
const logPersonWithAddres = (person: Person<Address>): Person<Address> => {
// TODO
}
const logPersonOrAddress = (person: Person | Address): Person | Address => {
// TODO
}
const logPersonAndAddress = (person: Person, address: Address) => {
// TODO
}
@@ -1,35 +0,0 @@
// @ts-nocheck
class Group {
getPersonAddress(person: Person): Address {
// TODO
}
getHardcodedAddress(): Address {
// TODO
}
addPerson(person: Person) {
// TODO
}
addPeople(people: Person[]) {
// TODO
}
getAddresses(people: Person[]): Address[] {
// TODO
}
logPersonWithAddress(person: Person<Address>): Person<Address> {
// TODO
}
logPersonOrAddress(person: Person | Address): Person | Address {
// TODO
}
logPersonAndAddress(person: Person, address: Address) {
// TODO
}
}
@@ -1,9 +0,0 @@
// @ts-nocheck
class Group extends BaseClass {}
class Group implements FirstInterface {}
class Group extends BaseClass implements FirstInterface, SecondInterface {}
class Group extends BaseClass<User> implements FirstInterface<User> {}
@@ -1,33 +0,0 @@
// @ts-nocheck
function getAddress(person: Person): Address {
// TODO
}
function getFirstAddress(people: Person[]): Address {
// TODO
}
function logPerson(person: Person) {
// TODO
}
function getHardcodedAddress(): Address {
// TODO
}
function getAddresses(people: Person[]): Address[] {
// TODO
}
function logPersonWithAddress(person: Person<Address>): Person<Address> {
// TODO
}
function logPersonOrAddress(person: Person | Address): Person | Address {
// TODO
}
function logPersonAndAddress(person: Person, address: Address) {
// TODO
}
@@ -1,33 +0,0 @@
// @ts-nocheck
function* getAddress(person: Person): Address {
// TODO
}
function* getFirstAddress(people: Person[]): Address {
// TODO
}
function* logPerson(person: Person) {
// TODO
}
function* getHardcodedAddress(): Address {
// TODO
}
function* getAddresses(people: Person[]): Address[] {
// TODO
}
function* logPersonWithAddress(person: Person<Address>): Person<Address> {
// TODO
}
function* logPersonOrAddress(person: Person | Address): Person | Address {
// TODO
}
function* logPersonAndAddress(person: Person, address: Address) {
// TODO
}
@@ -1,98 +1,3 @@
export const FUNCTIONS = [
{
nodeType: "function_definition with argument and return type",
fileName: "python/functions.py",
language: "Python",
cursorPosition: { line: 15, character: 8 },
definitionPositions: [
{ row: 14, column: 30 }, // Person
{ row: 14, column: 42 }, // Address
],
},
{
nodeType: "function_definition with generic argument and generic return type",
fileName: "python/functions.py",
language: "Python",
cursorPosition: { line: 18, character: 8 },
definitionPositions: [
{ row: 17, column: 35 }, // Group
{ row: 17, column: 42 }, // Person
{ row: 17, column: 53 }, // Group
{ row: 17, column: 61 }, // Address
],
},
{
nodeType: "function_definition with single argument and None return type",
fileName: "python/functions.py",
language: "Python",
cursorPosition: { line: 21, character: 8 },
definitionPositions: [
{ row: 20, column: 29 }, // Person
],
},
{
nodeType: "function_definition with no arguments and single return type",
fileName: "python/functions.py",
language: "Python",
cursorPosition: { line: 24, character: 8 },
definitionPositions: [
{ row: 23, column: 38 }, // Address
],
},
{
nodeType: "function_definition with Union arguments and Union return type",
fileName: "python/functions.py",
language: "Python",
cursorPosition: { line: 27, character: 8 },
definitionPositions: [
{ row: 26, column: 45 }, // Person
{ row: 26, column: 54 }, // Address
{ row: 26, column: 72 }, // Person
{ row: 26, column: 81 }, // Address
],
},
{
nodeType: "function_definition with multiple arguments and None return type",
fileName: "python/functions.py",
language: "Python",
cursorPosition: { line: 30, character: 8 },
definitionPositions: [
{ row: 29, column: 41 }, // Person
{ row: 29, column: 59 }, // Address
],
},
{
nodeType: "function_definition with one argument and Generator return type",
fileName: "python/functions.py",
language: "Python",
cursorPosition: { line: 33, character: 9 },
definitionPositions: [
{ row: 32, column: 40 }, // Person
{ row: 32, column: 62 }, // Address
],
},
{
nodeType: "function_definition inside a class",
fileName: "python/functions.py",
language: "Python",
cursorPosition: { line: 38, character: 12 },
definitionPositions: [
{ row: 37, column: 51 }, // Person
{ row: 37, column: 69 }, // Address
],
},
{
nodeType: "function_definition of an async function",
fileName: "python/functions.py",
language: "Python",
cursorPosition: { line: 41, character: 8 },
definitionPositions: [
{ row: 40, column: 37 }, // Address
{ row: 40, column: 48 }, // Person
],
},
]
export const CLASSES = [
{
nodeType: "class_definition with multiple superclasses",
@@ -1,82 +0,0 @@
import { streamLines } from "../../../diff/util"
import { HelperVars } from "../../util/HelperVars"
import { stopAtStartOf, stopAtStopTokens } from "./charStream"
import {
avoidEmptyComments,
avoidPathLine,
noDoubleNewLine,
showWhateverWeHaveAtXMs,
skipPrefixes,
stopAtLines,
stopAtLinesExact,
stopAtRepeatingLines,
stopAtSimilarLine,
streamWithNewLines,
} from "./lineStream"
const STOP_AT_PATTERNS = ["diff --git"]
export class StreamTransformPipeline {
async *transform(
generator: AsyncGenerator<string>,
prefix: string,
suffix: string,
multiline: boolean,
stopTokens: string[],
fullStop: () => void,
helper: HelperVars,
): AsyncGenerator<string> {
let charGenerator = generator
charGenerator = stopAtStopTokens(generator, [...stopTokens, ...STOP_AT_PATTERNS])
charGenerator = stopAtStartOf(charGenerator, suffix)
for (const charFilter of helper.lang.charFilters ?? []) {
charGenerator = charFilter({
chars: charGenerator,
prefix,
suffix,
filepath: helper.filepath,
multiline,
})
}
let lineGenerator = streamLines(charGenerator)
lineGenerator = stopAtLines(lineGenerator, fullStop)
const lineBelowCursor = this.getLineBelowCursor(helper)
if (lineBelowCursor.trim() !== "") {
lineGenerator = stopAtLinesExact(lineGenerator, fullStop, [lineBelowCursor])
}
lineGenerator = stopAtRepeatingLines(lineGenerator, fullStop)
lineGenerator = avoidEmptyComments(lineGenerator, helper.lang.singleLineComment)
lineGenerator = avoidPathLine(lineGenerator, helper.lang.singleLineComment)
lineGenerator = skipPrefixes(lineGenerator)
lineGenerator = noDoubleNewLine(lineGenerator)
for (const lineFilter of helper.lang.lineFilters ?? []) {
lineGenerator = lineFilter({ lines: lineGenerator, fullStop })
}
lineGenerator = stopAtSimilarLine(lineGenerator, this.getLineBelowCursor(helper), fullStop)
const timeoutValue = helper.options.modelTimeout
lineGenerator = showWhateverWeHaveAtXMs(lineGenerator, timeoutValue!)
const finalGenerator = streamWithNewLines(lineGenerator)
for await (const update of finalGenerator) {
yield update
}
}
private getLineBelowCursor(helper: HelperVars): string {
let lineBelowCursor = ""
let i = 1
while (lineBelowCursor.trim() === "" && helper.pos.line + i <= helper.fileLines.length - 1) {
lineBelowCursor = helper.fileLines[Math.min(helper.pos.line + i, helper.fileLines.length - 1)]
i++
}
return lineBelowCursor
}
}
@@ -1,192 +0,0 @@
import { describe, expect, it } from "vitest"
import { stopAtStartOf, stopAtStopTokens } from "./charStream"
async function* createMockStream(chunks: string[]): AsyncGenerator<string> {
for (const chunk of chunks) {
yield chunk
}
}
async function streamToString(stream: AsyncGenerator<string>): Promise<string> {
let result = ""
for await (const chunk of stream) {
result += chunk
}
return result
}
describe("stopAtStopTokens", () => {
it("should yield characters until a stop token is encountered", async () => {
const mockStream = createMockStream(["Hello", " world", "! Stop", "here"])
const stopTokens = ["Stop"]
const result = stopAtStopTokens(mockStream, stopTokens)
const output = []
for await (const char of result) {
output.push(char)
}
expect(output.join("")).toBe("Hello world! ")
})
it("should handle multiple stop tokens", async () => {
const mockStream = createMockStream(["This", " is a ", "test. END", " of stream"])
const stopTokens = ["END", "STOP", "HALT"]
const result = stopAtStopTokens(mockStream, stopTokens)
expect(await streamToString(result)).toBe("This is a test. ")
})
it("should handle stop tokens split across chunks", async () => {
const mockStream = createMockStream(["Hello", " wo", "r", "ld! ST", "OP now"])
const stopTokens = ["STOP"]
const result = stopAtStopTokens(mockStream, stopTokens)
const output = []
for await (const char of result) {
output.push(char)
}
expect(output.join("")).toBe("Hello world! ")
})
it("should yield all characters if no stop token is encountered", async () => {
const mockStream = createMockStream(["This", " is ", "a complete", " stream"])
const stopTokens = ["END"]
const result = stopAtStopTokens(mockStream, stopTokens)
expect(await streamToString(result)).toBe("This is a complete stream")
})
it("should handle empty chunks", async () => {
const mockStream = createMockStream(["Hello", "", " world", "", "! STOP"])
const stopTokens = ["STOP"]
const result = stopAtStopTokens(mockStream, stopTokens)
expect(await streamToString(result)).toBe("Hello world! ")
})
it("should handle stop token at the beginning of the stream", async () => {
const mockStream = createMockStream(["STOP", "Hello world"])
const stopTokens = ["STOP"]
const result = stopAtStopTokens(mockStream, stopTokens)
const output = []
for await (const char of result) {
output.push(char)
}
expect(output.join("")).toBe("")
})
it("should handle stop token at the end of the stream", async () => {
const mockStream = createMockStream(["Hello world", "STOP"])
const stopTokens = ["STOP"]
const result = stopAtStopTokens(mockStream, stopTokens)
expect(await streamToString(result)).toBe("Hello world")
})
it("should handle multiple stop tokens of different lengths", async () => {
const mockStream = createMockStream(["This is a ", "test with ", "multiple STOP", " tokens END"])
const stopTokens = ["STOP", "END", "HALT"]
const result = stopAtStopTokens(mockStream, stopTokens)
expect(await streamToString(result)).toBe("This is a test with multiple ")
})
it("should handle an empty stream", async () => {
const mockStream = createMockStream([])
const stopTokens = ["STOP"]
const result = stopAtStopTokens(mockStream, stopTokens)
expect(await streamToString(result)).toBe("")
})
it("should handle an empty stop tokens array", async () => {
const mockStream = createMockStream(["Hello", " world!"])
const stopTokens: string[] = []
const result = stopAtStopTokens(mockStream, stopTokens)
const output = []
for await (const char of result) {
output.push(char)
}
expect(output.join("")).toBe("Hello world!")
})
it("should handle stop token when remaining buffer is smaller than maximum stop token length", async () => {
const mockStream = createMockStream(["Hello world!STOP"])
const stopTokens: string[] = ["STOP", "STOP_TOKEN_THAT_IS_LARGER_THAN_BUFFER"]
const result = stopAtStopTokens(mockStream, stopTokens)
expect(await streamToString(result)).toBe("Hello world!")
})
})
describe("stopAtStartOf", () => {
const sampleCode = ` {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: \`Bearer \${this.workOsAccessToken}\`,
},
},
);
const data = await response.json();
return data.items;
}
async getContextItems(
query: string,
extras: ContextProviderExtras,
): Promise<ContextItem[]> {
const response = await extras.fetch(
new URL(
\`/proxy/context/\${this.options.id}/retrieve\`,
controlPlaneEnv.CONTROL_PLANE_URL,
),
`
/* Some LLMs, such as Codestral, repeat the suffix of the query. To test our filtering, we cut the sample code at random positions, remove a part of the input
and construct a response, containing the removed part and the suffix. The goal of the stopAtStartOf() method is to detect the start of the suffix in the response */
it("should stop if the start of the suffix is reached", async () => {
const suffix = `
const data = await response.json();
return data.items;
}`
const mockStream = createMockStream(sampleCode.split(/(?! )/g))
const result = stopAtStartOf(mockStream, suffix)
const resultStr = await streamToString(result)
expect(resultStr).toBe(` {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: \`Bearer \${this.workOsAccessToken}\`,
},
},
);
`)
})
it("should stop if the start of the suffix is reached, even if the suffix has a prefix", async () => {
const suffix = `
xxxconst data = await response.json();
return data.items;
}`
const mockStream = createMockStream(sampleCode.split(/(?! )/g))
const result = stopAtStartOf(mockStream, suffix)
const resultStr = await streamToString(result)
expect(resultStr).toBe(` {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: \`Bearer \${this.workOsAccessToken}\`,
},
},
);
`)
})
})
@@ -1,96 +0,0 @@
/**
* Asynchronously yields characters from the input stream, stopping if a stop token is encountered.
*
* @param {AsyncGenerator<string>} stream - The input stream of characters.
* @param {string[]} stopTokens - Array of tokens that signal when to stop yielding.
* @yields {string} Characters from the input stream.
* @returns {AsyncGenerator<string>} An async generator that yields characters until a stop condition is met.
* @description
* 1. If no stop tokens are provided, yields all characters from the stream.
* 2. Otherwise, buffers incoming chunks and checks for stop tokens.
* 3. Yields characters one by one if no stop token is found at the start of the buffer.
* 4. Stops yielding and returns if a stop token is encountered.
* 5. After the stream ends, filters encountered stop tokens in remaining buffer.
* 6. Yields any remaining buffered characters.
*/
export async function* stopAtStopTokens(stream: AsyncGenerator<string>, stopTokens: string[]): AsyncGenerator<string> {
if (stopTokens.length === 0) {
for await (const char of stream) {
yield char
}
return
}
const maxStopTokenLength = Math.max(...stopTokens.map((token) => token.length))
let buffer = ""
for await (const chunk of stream) {
buffer += chunk
while (buffer.length >= maxStopTokenLength) {
let found = false
for (const stopToken of stopTokens) {
if (buffer.startsWith(stopToken)) {
found = true
return
}
}
if (!found) {
yield buffer[0]
buffer = buffer.slice(1)
}
}
}
// Filter out the possible stop tokens from remaining buffer
stopTokens.forEach((token) => {
buffer = buffer.replace(token, "")
})
// Yield any remaining characters in the buffer
for (const char of buffer) {
yield char
}
}
/**
* Asynchronously yields characters from the input stream.
* Stops if the beginning of the suffix is detected in the stream.
*/
export async function* stopAtStartOf(
stream: AsyncGenerator<string>,
suffix: string,
sequenceLength: number = 20,
): AsyncGenerator<string> {
if (suffix.length < sequenceLength) {
for await (const chunk of stream) {
yield chunk
}
return
}
// We use sequenceLength * 1.5 as a heuristic to make sure we don't miss the sequence if the
// stream is not perfectly aligned with the sequence (small whitespace differences etc).
const targetPart = suffix.trimStart().slice(0, Math.floor(sequenceLength * 1.5))
let buffer = ""
for await (const chunk of stream) {
buffer += chunk
// Check if the targetPart contains contains the buffer at any point
if (buffer.length >= sequenceLength && targetPart.includes(buffer)) {
return // Stop processing when the sequence is found
}
// Yield chunk by chunk, ensuring not to exceed sequenceLength in the buffer
while (buffer.length > sequenceLength) {
yield buffer[0]
buffer = buffer.slice(1)
}
}
// Yield the remaining buffer if it is not contained in the `targetPart`
if (buffer.length > 0) {
yield buffer
}
}
@@ -1,4 +1,4 @@
import { LineStream } from "../../../diff/util"
import type { LineStream } from "../../../diff/util"
import { lineIsRepeated } from "../../util/textSimilarity"
export { lineIsRepeated }
@@ -177,19 +177,6 @@ export async function* stopAtLines(
}
}
/**
* Yield until an exact stop line is encountered, then call fullStop.
*/
export async function* stopAtLinesExact(stream: LineStream, fullStop: () => void, linesToStopAt: string[]): LineStream {
for await (const line of stream) {
if (linesToStopAt.some((stopAt) => line === stopAt)) {
fullStop()
break
}
yield line
}
}
/**
* On the first line only, strip any configured prefix (e.g. "<COMPLETION>").
*/
@@ -230,41 +217,3 @@ export async function* stopAtRepeatingLines(lines: LineStream, fullStop: () => v
previousLine = line
}
}
/**
* Pass through lines, but if the stream takes longer than ms after we have at least one non-empty line, stop early.
*/
export async function* showWhateverWeHaveAtXMs(lines: LineStream, ms: number): LineStream {
const startTime = Date.now()
let firstNonWhitespaceLineYielded = false
for await (const line of lines) {
yield line
if (!firstNonWhitespaceLineYielded && line.trim() !== "") {
firstNonWhitespaceLineYielded = true
}
const isTakingTooLong = Date.now() - startTime > ms
if (isTakingTooLong && firstNonWhitespaceLineYielded) {
break
}
}
}
/**
* Yield lines until the first blank line after some content; then stop.
*/
export async function* noDoubleNewLine(lines: LineStream): LineStream {
let isFirstLine = true
for await (const line of lines) {
if (line.trim() === "" && !isFirstLine) {
return
}
isFirstLine = false
yield line
}
}
@@ -1,60 +0,0 @@
##### Prompt #####
{
"active": true,
"department": "Product Development",
"location": {
"country": "USA",
"state": "California",
"city": "San BERNARDINO",
"coordinates": {
<FIM>
}
},
"employees": [
{
"name": "John Doe",
"age": 30,
"position": "Developer",
"skills": ["JavaScript", "React", "Node.js"],
"remote": false,
"salary": {
"currency": "USD",
"amount": 95000
}
},
{
"name": "Jane Smith",
"age": 25,
"position": "Designer",
"skills": ["Photoshop", "Illustrator"],
"remote": true,
"salary": {
"currency": "USD",
"amount": 70000
}
},
{
"name": "Emily Johnson",
"age": 35,
"position": "Manager",
"teamSize": 8,
"remote": true,
"skills": ["Leadership", "Project Management"],==========================================================================
==========================================================================
Completion:
"latitude": 34.10834,
"longitude": -117.28977
}
},
"employeeCount": 2,
"averageAge": 30,
"remoteFriendly": true,
"salaryRange": {
"min": 70000,
"max": 95000,
"currency": "USD"
},
"skills": {
"required": ["JavaScript", "React", "Node.js", "Leadership", "Project Management"],
"optional": ["Photoshop", "Illustrator"]
@@ -1,128 +0,0 @@
##### Prompt #####
}`,
},
{
description: "Should autocomplete Vue computed property",
filename: "UserComponent.vue",
input: `<template>
<div>
<p>User Full Name: {{ fullName }}</p>
</div>
</template>
<script>
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe',
};
},
computed: {
fullName<|fim|>
},
};
</script>
`,
llmOutput: `() {
return this.firstName + ' ' + this.lastName;
}`,
expectedCompletion: `() {
return this.firstName + ' ' + this.lastName;
}`,
},
{
description: "Should autocomplete Vue method using props",
filename: "TodoItem.vue",
input: `<template>
<li>
<p>{{ title }}</p>
<button @click="completeTodo">Complete</button>
</li>
</template>
<script>
export default {
props: {
title: String,
completed: Boolean,
},
methods: {
completeTodo() {
<|fim|> = true;
}
},
};
</script>
`,
llmOutput: `this.completed`,
expectedCompletion: `this.completed`,
},
{
description: "Should autocomplete Svelte reactive statement",
filename: "Counter.svelte",
input: `
<script>
let count = 0;
$: <|fim|>
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>
Clicked {count} times
</button>
`,
llmOutput: `doubledCount = count * 2`,
expectedCompletion: `doubledCount = count * 2`,
},
{
description: "Should autocomplete Svelte component inside HTML",
filename: "NestedComponent.svelte",
input: `
<script>
import ChildComponent from './ChildComponent.svelte';
</script>
<main>
<h1>Hello Svelte</h1>
<ChildComponent <|fim|> />
</main>
`,
llmOutput: `name="World"`,
expectedCompletion: `name="World"`,
},
{
description: "Should handle autocomplete in Svelte each block",
filename: "List.svelte",
input: `
<script>
let items = ["Apple", "Banana", "Cherry"];
</script>
<ul>
{#each items as item}
<li>{item}</li>
{/each<|fim|>
</ul>
`,
llmOutput: `}`,
expectedCompletion: `}`,
},
<FIM>
];
==========================================================================
==========================================================================
Completion:
export default {
components: {
ChildComponent,
},
@@ -1,20 +0,0 @@
##### Prompt #####
{
"employees": [
{ "name": "John Doe", "age": 30, "position": "Developer" },
{ "name": "Jane Smith", "age": 25, "position": "Designer" },
{ "name": "Emily Johnson", "age": 35, "position": "Manager" }<FIM>
],
"active": true
}
==========================================================================
==========================================================================
Completion:
}
{
"employees": [
{ "name": "John Doe", "age": 30 },
{ "name": "Jane Smith", "age": 25 }
@@ -1,29 +0,0 @@
##### Prompt #####
class Calculator:
def __init__(self):
self.result = 0
def add(self, number):
self.result += number
return self
def divid<FIM>
def subtract(self, number):
self.result -= number
return self
def reset(self):
self.result = 0
return self
def get_result(self):
return self.result
==========================================================================
==========================================================================
Completion:
self.result /= number
return self
@@ -1,41 +0,0 @@
##### Prompt #####
class Calculator
attr_accessor :result
def initialize
@result = 0
end
def add(number)
@result += number
self
end
def subtract(number)
@result -= number
self
end
def power(ne<FIM>
def get_result
@result
end
def reset
@result = 0
self
end
end
calc = Calculator.new
calc.add(5).subtract(3)
puts "Result: #{calc.get_result}"
==========================================================================
==========================================================================
Completion:
@result ** number
self
end
@@ -1,73 +0,0 @@
##### Prompt #####
use std::io;
fn main() {
println!("Welcome to the Calculator!");
loop {
println!("Please enter an operator (+, -, *, /) or 'q' to quit:");
let operator = read_input();
if operator == "q" {
break;
}
println!("Please enter the first number:");
let num1 = read_input().parse::<f64>().unwrap();
println!("Please enter the second number:");
let num2 = read_input().parse::<f64>().unwrap();
let result = match operator.as_str() {
"+" => num1 + num2,
"-" => num1 - num2,
"*" => num1 * num2,
"/" => num1 / num2,
<FIM>
_ => {
println!("Invalid operator. Please try again.");
continue;
}
};
println!("Result: {}", result);
}
}
fn read_input() -> String {
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("Failed to read input");
input.trim().to_string()
}
==========================================================================
==========================================================================
Completion:
=> {
println!("Invalid operator. Please try again.");
continue;
}
};
println!("Result: {}", result);
}
println!("Goodbye!");
}
fn main2() {
println!("Welcome to the Calculator!");
loop {
println!("Please enter an operator (+, -, *, /) or 'q' to quit:");
let operator = read_input();
if operator == "q" {
break;
}
match operator.as_str() {
"+" | "-" | "*" | "/" => (),
@@ -1,37 +0,0 @@
import { afterAll, beforeAll, describe, it } from "vitest"
import { addToTestDir, setUpTestDir, tearDownTestDir } from "../../../test/testDir"
import { TEST_CASES_WITH_DIFF, TEST_CASES_WITHOUT_DIFF } from "./testCases"
import { AutocompleteFileringTestInput, testAutocompleteFiltering } from "./util"
const filterTestCases = (tests: AutocompleteFileringTestInput[]) => {
if (tests.some((test) => test.options?.only)) {
return tests.filter((test) => test.options?.only)
}
return tests
}
describe("Autocomplete filtering tests", () => {
beforeAll(async () => {
tearDownTestDir()
setUpTestDir()
addToTestDir([".continueignore"])
})
afterAll(async () => {
tearDownTestDir()
})
describe("Should return unmodified LLM output", () => {
it.each(filterTestCases(TEST_CASES_WITHOUT_DIFF))("$description", async (testCase) => {
await testAutocompleteFiltering(testCase)
})
})
describe("Should return modified LLM output", () => {
it.each(filterTestCases(TEST_CASES_WITH_DIFF))("$description", async (testCase) => {
await testAutocompleteFiltering(testCase)
})
})
})
@@ -1,81 +0,0 @@
import { expect } from "vitest"
import { MockLLM } from "../../../llm/llms/Mock"
import { testMinimalConfigProvider, testIde } from "../../../test/fixtures"
import { joinPathsToUri } from "../../../util/uri"
import { CompletionProvider } from "../../CompletionProvider"
import { AutocompleteInput } from "../../util/types"
const FIM_DELIMITER = "<|fim|>"
function parseFimExample(text: string): { prefix: string; suffix: string } {
const [prefix, suffix] = text.split(FIM_DELIMITER)
return { prefix, suffix }
}
export interface AutocompleteFileringTestInput {
description: string
filename: string
input: string
llmOutput: string
expectedCompletion: string | null | undefined
options?: {
only?: boolean
}
}
export async function testAutocompleteFiltering(test: AutocompleteFileringTestInput) {
// Normalize line endings to LF for cross-platform compatibility (Windows Git may check out CRLF)
const normalizedInput = test.input.replace(/\r\n/g, "\n")
const normalizedLlmOutput = test.llmOutput.replace(/\r\n/g, "\n")
const { prefix } = parseFimExample(normalizedInput)
// Setup necessary objects
const llm = new MockLLM({
model: "mock",
})
llm.completion = normalizedLlmOutput
const ide = testIde
const configHandler = testMinimalConfigProvider
// Create a real file
const [workspaceDir] = await ide.getWorkspaceDirs()
const fileUri = joinPathsToUri(workspaceDir, test.filename)
await ide.writeFile(fileUri, normalizedInput.replace(FIM_DELIMITER, ""))
// Prepare completion input and provider
const completionProvider = new CompletionProvider(
configHandler,
ide,
async () => llm,
() => {},
async () => [],
)
const line = prefix.split("\n").length - 1
const character = prefix.split("\n")[line].length
const autocompleteInput: AutocompleteInput = {
isUntitledFile: false,
completionId: "test-completion-id",
filepath: fileUri,
pos: {
line,
character,
},
recentlyEditedRanges: [],
recentlyVisitedRanges: [],
}
// Generate a completion
const result = await completionProvider.provideInlineCompletionItems(
autocompleteInput,
undefined,
true, // force=true to skip debounce in tests
)
// Ensure that we return the text that is wanted to be displayed
// Normalize line endings for cross-platform compatibility
const normalizeLineEndings = (str: string | null | undefined) => str?.replace(/\r\n/g, "\n")
expect(normalizeLineEndings(result?.completion)).toEqual(normalizeLineEndings(test.expectedCompletion))
}
@@ -1,79 +0,0 @@
import { CompletionOptions, ILLM } from "../.."
import { StreamTransformPipeline } from "../filtering/streamTransforms/StreamTransformPipeline"
import { HelperVars } from "../util/HelperVars"
import { GeneratorReuseManager } from "./GeneratorReuseManager"
import { stopAfterMaxProcessingTime } from "./utils"
export class CompletionStreamer {
private streamTransformPipeline = new StreamTransformPipeline()
private generatorReuseManager: GeneratorReuseManager
constructor(onError: (err: unknown) => void) {
this.generatorReuseManager = new GeneratorReuseManager(onError)
}
async *streamCompletionWithFilters(
token: AbortSignal,
llm: ILLM,
prefix: string,
suffix: string,
prompt: string,
multiline: boolean,
completionOptions: Partial<CompletionOptions> | undefined,
helper: HelperVars,
) {
// Full stop means to stop the LLM's generation, instead of just truncating the displayed completion
const fullStop = () => this.generatorReuseManager.currentGenerator?.cancel()
// Try to reuse pending requests if what the user typed matches start of completion
const generator = this.generatorReuseManager.getGenerator(
prefix,
(abortSignal: AbortSignal) => {
const generator = llm.supportsFim()
? llm.streamFim(prefix, suffix, abortSignal, completionOptions)
: llm.streamComplete(prompt, abortSignal, {
...completionOptions,
raw: true,
})
/**
* This transformer applies even on reused generator. We are deliberately
* not using streamTransformPipeline because we want to capture and stop
* the request even if the generator is being reused.
*/
return helper.options.transform
? stopAfterMaxProcessingTime(generator, helper.options.modelTimeout * 2.5, fullStop)
: generator
},
multiline,
)
// LLM
const generatorWithCancellation = async function* () {
for await (const update of generator) {
if (token.aborted) {
return
}
yield update
}
}
const initialGenerator = generatorWithCancellation()
const transformedGenerator = helper.options.transform
? this.streamTransformPipeline.transform(
initialGenerator,
prefix,
suffix,
multiline,
completionOptions?.stop || [],
fullStop,
helper,
)
: initialGenerator
for await (const update of transformedGenerator) {
yield update
}
}
}
@@ -1,208 +0,0 @@
import { afterEach, beforeEach, describe, expect, Mock, test, vi } from "vitest"
import { GeneratorReuseManager } from "./GeneratorReuseManager"
function createMockGenerator(data: string[], delay: number = 0): (abortSignal: AbortSignal) => AsyncGenerator<string> {
const mockGenerator = async function* () {
for (const chunk of data) {
yield chunk
if (delay > 0) {
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
}
const newGenerator = vi.fn<() => AsyncGenerator<string>>().mockReturnValue(mockGenerator())
return newGenerator
}
describe("GeneratorReuseManager", () => {
let reuseManager: GeneratorReuseManager
let onErrorMock: Mock
beforeEach(() => {
onErrorMock = vi.fn()
reuseManager = new GeneratorReuseManager(onErrorMock)
})
afterEach(() => {
vi.clearAllMocks()
})
test("creates new generator when there is no current generator", async () => {
const data = ["hello ", "world"]
const newGenerator = createMockGenerator(data)
const prefix = ""
const generator = reuseManager.getGenerator(prefix, newGenerator, true)
const output: string[] = []
for await (const chunk of generator) {
output.push(chunk)
}
expect(output).toEqual(data)
expect(newGenerator).toHaveBeenCalledTimes(1)
})
test("reuses generator when prefix matches pending completion", async () => {
const newGenerator = createMockGenerator(["llo ", "world"])
// First call with initial prefix
const prefix1 = "he"
const generator1 = reuseManager.getGenerator(prefix1, newGenerator, true)
const output1: string[] = []
for await (const chunk of generator1) {
output1.push(chunk)
}
expect(output1).toEqual(["llo ", "world"])
// Second call with extended prefix that matches pending completion
const prefix2 = "hello "
const generator2 = reuseManager.getGenerator(prefix2, newGenerator, true)
const output2: string[] = []
for await (const chunk of generator2) {
output2.push(chunk)
}
expect(output2).toEqual(["world"])
// Ensure generator was reused (newGenerator should be called only once)
expect(newGenerator).toHaveBeenCalledTimes(1)
})
test("creates new generator when prefix does not match pending completion", async () => {
const data = ["goodbye ", "world"]
const newGenerator = createMockGenerator(data)
// Initial generator with different prefix
reuseManager.pendingGeneratorPrefix = "hello "
reuseManager.pendingCompletion = "world"
const prefix = "good"
const generator = reuseManager.getGenerator(prefix, newGenerator, true)
const output: string[] = []
for await (const chunk of generator) {
output.push(chunk)
}
expect(output).toEqual(data)
// Ensure a new generator was created
expect(newGenerator).toHaveBeenCalledTimes(1)
})
test("handles multiline=false by stopping at newline", async () => {
const data = ["first line\n", "second line"]
const newGenerator = createMockGenerator(data)
const prefix = ""
const generator = reuseManager.getGenerator(prefix, newGenerator, false)
const output: string[] = []
for await (const chunk of generator) {
output.push(chunk)
}
expect(output).toEqual(["first line"])
// Ensure it stops after the first newline
})
test("handles multiline=true by not stopping at newline", async () => {
const data = ["first line\n", "second line"]
const newGenerator = createMockGenerator(data)
const prefix = ""
const generator = reuseManager.getGenerator(prefix, newGenerator, true)
const output: string[] = []
for await (const chunk of generator) {
output.push(chunk)
}
expect(output).toEqual(data)
})
test("cancels previous generator when creating a new one", async () => {
const data1 = ["data from generator 1", "not generated"]
const data2 = ["data from generator 2"]
const newGenerator1 = createMockGenerator(data1, 1000) // Delay so we have the chance to cancel it
const newGenerator2 = createMockGenerator(data2)
const prefix1 = "prefix1"
const prefix2 = "prefix2"
// First generator
const generator1 = reuseManager.getGenerator(prefix1, newGenerator1, true)
const output1: string[] = []
for await (const chunk of generator1) {
output1.push(chunk)
// Simulate the generator being canceled before completing
reuseManager.currentGenerator?.cancel()
}
expect(output1.length).toEqual(1)
expect(output1[0]).toEqual(data1[0])
// Second generator
const generator2 = reuseManager.getGenerator(prefix2, newGenerator2, true)
const output2: string[] = []
for await (const chunk of generator2) {
output2.push(chunk)
}
expect(output2).toEqual(data2)
})
test("calls onError when generator throws an error", async () => {
const error = new Error("Generator error")
const mockGenerator = async function* () {
throw error
}
const newGenerator = vi.fn<() => AsyncGenerator<string>>().mockReturnValue(mockGenerator())
const prefix = ""
const generator = reuseManager.getGenerator(prefix, newGenerator, true)
const output: string[] = []
await expect(async () => {
for await (const chunk of generator) {
output.push(chunk)
}
}).not.toThrow() // getGenerator handles errors internally
expect(onErrorMock).toHaveBeenCalledWith(error)
expect(output).toEqual([])
})
test("handles backspacing by creating new generator when prefix is shorter", async () => {
const data = ["hello world"]
const newGenerator1 = createMockGenerator(data)
const newGenerator2 = createMockGenerator(data)
// First prefix
const prefix1 = "hello world"
const generator1 = reuseManager.getGenerator(prefix1, newGenerator1, true)
const output1: string[] = []
for await (const chunk of generator1) {
output1.push(chunk)
}
// Simulate backspace (prefix is shorter)
const prefix2 = "hello worl"
const generator2 = reuseManager.getGenerator(prefix2, newGenerator2, true)
const output2: string[] = []
for await (const chunk of generator2) {
output2.push(chunk)
}
// Ensure a new generator was created
expect(newGenerator1).toHaveBeenCalledTimes(1)
expect(newGenerator2).toHaveBeenCalledTimes(1)
})
})
@@ -1,70 +0,0 @@
import { ListenableGenerator } from "./ListenableGenerator"
export class GeneratorReuseManager {
currentGenerator: ListenableGenerator<string> | undefined
pendingGeneratorPrefix: string | undefined
pendingCompletion = ""
constructor(private readonly onError: (err: unknown) => void) {}
private _createListenableGenerator(abortController: AbortController, gen: AsyncGenerator<string>, prefix: string) {
this.currentGenerator?.cancel()
const listenableGen = new ListenableGenerator(gen, this.onError, abortController)
listenableGen.listen((chunk) => (this.pendingCompletion += chunk ?? ""))
this.pendingGeneratorPrefix = prefix
this.pendingCompletion = ""
this.currentGenerator = listenableGen
}
private shouldReuseExistingGenerator(prefix: string): boolean {
return (
!!this.currentGenerator &&
!!this.pendingGeneratorPrefix &&
(this.pendingGeneratorPrefix + this.pendingCompletion).startsWith(prefix) &&
// for e.g. backspace
this.pendingGeneratorPrefix?.length <= prefix?.length
)
}
async *getGenerator(
prefix: string,
newGenerator: (abortSignal: AbortSignal) => AsyncGenerator<string>,
multiline: boolean,
): AsyncGenerator<string> {
// If we can't reuse, then create a new generator
if (!this.shouldReuseExistingGenerator(prefix)) {
// Create a wrapper over the current generator to fix the prompt
const abortController = new AbortController()
this._createListenableGenerator(abortController, newGenerator(abortController.signal), prefix)
}
// Already typed characters are those that are new in the prefix from the old generator
let typedSinceLastGenerator = prefix.slice(this.pendingGeneratorPrefix?.length) || ""
for await (let chunk of this.currentGenerator?.tee() ?? []) {
if (!chunk) {
continue
}
// Ignore already typed characters in the completion
while (chunk.length && typedSinceLastGenerator.length) {
if (chunk[0] === typedSinceLastGenerator[0]) {
typedSinceLastGenerator = typedSinceLastGenerator.slice(1)
chunk = chunk.slice(1)
} else {
break
}
}
// Break at newline unless we are in multiline mode
const newLineIndex = chunk.indexOf("\n")
if (newLineIndex >= 0 && !multiline) {
yield chunk.slice(0, newLineIndex)
break
} else if (chunk !== "") {
yield chunk
}
}
}
}
@@ -1,148 +0,0 @@
import { describe, expect, it, vi } from "vitest"
import { ListenableGenerator } from "./ListenableGenerator"
describe("ListenableGenerator", () => {
// Helper function to create an async generator
async function* asyncGenerator<T>(values: T[]) {
for (const value of values) {
// Yield on next event loop iteration to ensure async behavior
await new Promise(setImmediate)
yield value
}
}
it("should yield values from the source generator via tee()", async () => {
const values = [1, 2, 3]
const source = asyncGenerator(values)
const onError = vi.fn()
const lg = new ListenableGenerator<number>(source, onError, new AbortController())
const result: number[] = []
for await (const value of lg.tee()) {
result.push(value)
}
expect(result).toEqual(values)
expect(onError).not.toHaveBeenCalled()
})
it("should allow listeners to receive values", async () => {
const values = [1, 2, 3]
const source = asyncGenerator(values)
const onError = vi.fn()
const lg = new ListenableGenerator<number>(source, onError, new AbortController())
const listener = vi.fn()
// Add listener after yielding starts (next event loop iteration)
await new Promise(setImmediate)
lg.listen(listener)
// Wait for generator to actually finish
await lg.waitForCompletion()
expect(listener).toHaveBeenCalledWith(1)
expect(listener).toHaveBeenCalledWith(2)
expect(listener).toHaveBeenCalledWith(3)
// Listener should receive null at the end
expect(listener).toHaveBeenCalledWith(null)
})
it("should buffer values for listeners added after some values have been yielded", async () => {
const values = [1, 2, 3]
const source = asyncGenerator(values)
const onError = vi.fn()
const lg = new ListenableGenerator<number>(source, onError, new AbortController())
const initialListener = vi.fn()
lg.listen(initialListener)
// Wait for the first value to be yielded (next event loop iteration)
await new Promise(setImmediate)
// Add a second listener after first value has been yielded
const newListener = vi.fn()
lg.listen(newListener)
// Wait for generator to actually finish
await lg.waitForCompletion()
// Both listeners should have received all values
;[initialListener, newListener].forEach((listener) => {
expect(listener).toHaveBeenCalledWith(1)
expect(listener).toHaveBeenCalledWith(2)
expect(listener).toHaveBeenCalledWith(3)
expect(listener).toHaveBeenCalledWith(null)
})
})
it("should handle cancellation", async () => {
const values = [1, 2, 3, 4, 5]
const source = asyncGenerator(values)
const onError = vi.fn()
const lg = new ListenableGenerator<number>(source, onError, new AbortController())
const result: number[] = []
const teeIterator = lg.tee()
const consume = async () => {
for await (const value of teeIterator) {
result.push(value)
if (value === 3) {
lg.cancel()
}
}
}
await consume()
expect(result).toEqual([1, 2, 3])
expect(lg["_isEnded"]).toBe(true)
})
it("should call onError when the source generator throws an error", async () => {
async function* errorGenerator() {
yield 1
throw new Error("Test error")
}
const source = errorGenerator()
const onError = vi.fn()
const lg = new ListenableGenerator<number>(source, onError, new AbortController())
const result: number[] = []
for await (const value of lg.tee()) {
result.push(value)
}
expect(result).toEqual([1])
expect(onError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(new Error("Test error"))
})
it("should notify listeners when the generator ends", async () => {
const values = [1, 2, 3]
const source = asyncGenerator(values)
const onError = vi.fn()
const lg = new ListenableGenerator<number>(source, onError, new AbortController())
const listener = vi.fn()
lg.listen(listener)
// Wait for the generator to actually finish
await lg.waitForCompletion()
expect(listener).toHaveBeenCalledWith(1)
expect(listener).toHaveBeenCalledWith(2)
expect(listener).toHaveBeenCalledWith(3)
expect(listener).toHaveBeenCalledWith(null)
})
})
@@ -1,84 +0,0 @@
export class ListenableGenerator<T> {
private _source: AsyncGenerator<T>
private _buffer: T[] = []
private _listeners: Set<(value: T) => void> = new Set()
private _isEnded = false
private _abortController: AbortController
private _completionPromise: Promise<void>
constructor(
source: AsyncGenerator<T>,
private readonly onError: (e: unknown) => void,
abortController: AbortController,
) {
this._source = source
this._abortController = abortController
this._completionPromise = this._start().catch((e) => console.log(`Listenable generator failed: ${e.message}`))
}
public cancel() {
this._abortController.abort()
this._isEnded = true
}
public waitForCompletion(): Promise<void> {
return this._completionPromise
}
private async _start() {
try {
for await (const value of this._source) {
if (this._isEnded) {
break
}
this._buffer.push(value)
for (const listener of this._listeners) {
listener(value)
}
}
} catch (e) {
this.onError(e)
} finally {
this._isEnded = true
for (const listener of this._listeners) {
listener(null as any)
}
}
}
listen(listener: (value: T) => void) {
this._listeners.add(listener)
for (const value of this._buffer) {
listener(value)
}
if (this._isEnded) {
listener(null as any)
}
}
async *tee(): AsyncGenerator<T> {
try {
let i = 0
while (i < this._buffer.length) {
yield this._buffer[i++]
}
while (!this._isEnded) {
let resolve: (value: T) => void
const promise = new Promise<T>((res) => {
resolve = res
this._listeners.add(resolve!)
})
await promise
this._listeners.delete(resolve!)
// Possible timing caused something to slip in between
// timers so we iterate over the buffer
while (i < this._buffer.length) {
yield this._buffer[i++]
}
}
} finally {
// this._listeners.delete(resolve!);
}
}
}
@@ -1,126 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { stopAfterMaxProcessingTime } from "./utils"
describe("stopAfterMaxProcessingTime", () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
async function* createMockStream(chunks: string[]): AsyncGenerator<string> {
for (const chunk of chunks) {
yield chunk
}
}
async function streamToString(stream: AsyncGenerator<string>): Promise<string> {
let result = ""
for await (const chunk of stream) {
result += chunk
}
return result
}
it("should yield all chunks when maxTimeMs is not reached", async () => {
const mockStream = createMockStream(["Hello", " world", "!"])
const fullStop = vi.fn()
const result = stopAfterMaxProcessingTime(mockStream, 1000, fullStop)
const output = await streamToString(result)
expect(output).toBe("Hello world!")
expect(fullStop).not.toHaveBeenCalled()
})
it("should stop processing after max time is reached", async () => {
// Mock implementation of Date.now
let currentTime = 0
const originalDateNow = Date.now
Date.now = vi.fn(() => currentTime)
// Create a generator that we can control
async function* controlledGenerator(): AsyncGenerator<string> {
for (let i = 0; i < 100; i++) {
// After yielding 10 chunks, simulate time passing beyond our limit
if (i === 10) {
currentTime = 1000 // This exceeds our 500ms limit
}
yield `chunk-${i}`
}
}
const fullStop = vi.fn()
const maxTimeMs = 500
const transformedGenerator = stopAfterMaxProcessingTime(controlledGenerator(), maxTimeMs, fullStop)
// Consume the generator and collect outputs
const outputs: string[] = []
for await (const chunk of transformedGenerator) {
outputs.push(chunk)
}
// We expect:
// 1. Not all chunks were processed (less than 100)
// 2. fullStop was called
// 3. We processed at least the chunks before time was exceeded
expect(outputs.length).toBeLessThan(100)
expect(outputs.length).toBeGreaterThanOrEqual(10) // We should get at least the first 10 chunks
expect(fullStop).toHaveBeenCalled()
// Restore Date.now
Date.now = originalDateNow
})
it("should check time only periodically based on checkInterval", async () => {
const chunks = Array(100).fill("x")
const mockStream = createMockStream(chunks)
const fullStop = vi.fn()
// Spy on Date.now to count how many times it's called
const dateSpy = vi.spyOn(Date, "now")
// Stream should complete normally (not hitting the timeout)
await streamToString(stopAfterMaxProcessingTime(mockStream, 10000, fullStop))
// The first call is to set startTime, then once every checkInterval (10) chunks
// So for 100 chunks, we expect startTime + ~10 checks = ~11 calls
// We use a range because implementation details might vary slightly
expect(dateSpy.mock.calls.length).toBeGreaterThanOrEqual(1)
expect(dateSpy.mock.calls.length).toBeLessThanOrEqual(15)
dateSpy.mockRestore()
})
it("should handle empty stream gracefully", async () => {
const mockStream = createMockStream([])
const fullStop = vi.fn()
const result = stopAfterMaxProcessingTime(mockStream, 1000, fullStop)
const output = await streamToString(result)
expect(output).toBe("")
expect(fullStop).not.toHaveBeenCalled()
})
it("should pass through all chunks if there's no timeout", async () => {
const chunks = Array(100).fill("test chunk")
const mockStream = createMockStream(chunks)
const fullStop = vi.fn()
// Use undefined as timeout to simulate no timeout
const result = stopAfterMaxProcessingTime(mockStream, undefined as any, fullStop)
// Process the stream
const processedChunks = []
for await (const chunk of result) {
processedChunks.push(chunk)
}
expect(processedChunks.length).toBe(chunks.length)
expect(fullStop).not.toHaveBeenCalled()
})
})
@@ -1,25 +0,0 @@
export async function* stopAfterMaxProcessingTime(
stream: AsyncGenerator<string>,
maxTimeMs: number,
fullStop: () => void,
): AsyncGenerator<string> {
const startTime = Date.now()
/**
* Check every 10 chunks to avoid performance overhead.
*/
const checkInterval = 10
let chunkCount = 0
for await (const chunk of stream) {
yield chunk
chunkCount++
if (chunkCount % checkInterval === 0) {
if (Date.now() - startTime > maxTimeMs) {
fullStop()
return
}
}
}
}
@@ -1,57 +0,0 @@
import ignore from "ignore"
import { getConfigJsonPath } from "../../util/paths"
import { findUriInDirs } from "../../util/uri"
import { HelperVars } from "../util/HelperVars"
async function isDisabledForFile(
currentFilepath: string,
disableInFiles: string[] | undefined,
workspaceDirs: string[],
) {
if (disableInFiles) {
// Relative path needed for `ignore`
const { relativePathOrBasename } = findUriInDirs(currentFilepath, workspaceDirs)
const pattern = ignore().add(disableInFiles)
if (pattern.ignores(relativePathOrBasename)) {
return true
}
}
return false
}
export async function shouldPrefilter(helper: HelperVars, workspaceDirs: string[]): Promise<boolean> {
// Allow disabling autocomplete from config.json
if (helper.options.disable) {
return true
}
// Check whether we're in the continue config.json file
if (helper.filepath === getConfigJsonPath()) {
return true
}
// Check whether autocomplete is disabled for this file
const disableInFiles = [
...(helper.options.disableInFiles ?? []),
"*.prompt",
// "some-example-ignored-file", //MINIMAL_REPO - was configurable
]
if (await isDisabledForFile(helper.filepath, disableInFiles, workspaceDirs)) {
return true
}
// Don't offer completions when we have no information (untitled file and no file contents)
if (helper.filepath.includes("Untitled") && helper.fileContents.trim() === "") {
return true
}
// if (
// helper.options.transform &&
// (await shouldLanguageSpecificPrefilter(helper))
// ) {
// return true;
// }
return false
}
@@ -1,262 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest"
// ---------- Module mocks ----------
// Simple Handlebars mock that does naive placeholder substitution
vi.mock("handlebars", () => {
return {
default: {
compile: (template: string) => (ctx: Record<string, string>) => {
return template
.replace(/{{prefix}}/g, ctx.prefix)
.replace(/{{suffix}}/g, ctx.suffix)
.replace(/{{filename}}/g, ctx.filename ?? "")
.replace(/{{reponame}}/g, ctx.reponame ?? "")
.replace(/{{language}}/g, ctx.language ?? "")
},
},
}
})
// Token utilities – we map 1 char = 1 token for simplicity
vi.mock("../../../llm/countTokens", () => {
const countTokens = (str: string) => str.length
const pruneLinesFromTop = (str: string, allowed: number) => str.slice(Math.max(0, str.length - allowed))
const pruneLinesFromBottom = (str: string, allowed: number) => str.slice(0, allowed)
const getTokenCountingBufferSafety = () => 0
return {
countTokens,
pruneLinesFromTop,
pruneLinesFromBottom,
getTokenCountingBufferSafety,
}
})
// Snippet selection – configurable via constant return value
vi.mock("../filtering", () => ({
getSnippets: () => [],
}))
// Snippet formatting
const FORMATTED_SNIPPETS = "[FORMATTED_SNIPPETS]"
vi.mock("../formatting", () => ({
formatSnippets: () => FORMATTED_SNIPPETS,
}))
// Stop tokens helper – we expose a variable so each test can override it
let stopTokenReturn: string[] = ["<STOP>"]
vi.mock("../getStopTokens", () => ({
getStopTokens: () => stopTokenReturn,
}))
// AutocompleteTemplate – provide overridable template + compiler + completionOptions
let templateOverride: any = (prefix: string, suffix: string) => `${prefix}|${suffix}`
let compileFnOverride: ((...args: any[]) => [string, string]) | undefined
let completionOptionsOverride: Record<string, any> | undefined
vi.mock("../AutocompleteTemplate", () => ({
getTemplateForModel: () => ({
template: templateOverride,
compilePrefixSuffix: compileFnOverride,
completionOptions: completionOptionsOverride ?? {},
}),
}))
// ---------- Imports after mocks ----------
import { renderPrompt, renderPromptWithTokenLimit } from ".."
import { AutocompleteLanguageInfo } from "../../constants/AutocompleteLanguageInfo"
import { SnippetPayload } from "../../snippets"
import { HelperVars } from "../../util/HelperVars"
// ---------- Helper builders ----------
const tsLang: AutocompleteLanguageInfo = {
name: "TypeScript",
topLevelKeywords: [],
singleLineComment: "//",
endOfLine: [";"],
}
const emptySnippetPayload: SnippetPayload = {
rootPathSnippets: [],
importDefinitionSnippets: [],
ideSnippets: [],
recentlyEditedRangeSnippets: [],
recentlyVisitedRangesSnippets: [],
diffSnippets: [],
clipboardSnippets: [],
recentlyOpenedFileSnippets: [],
staticSnippet: [],
}
function makeHelper() {
return {
input: {
filepath: "file:///test.ts",
pos: { line: 0, character: 0 },
recentlyEditedRanges: [],
recentlyVisitedRanges: [],
},
prunedPrefix: "PRUNED_PREFIX",
prunedSuffix: "PRUNED_SUFFIX",
lang: tsLang,
modelName: "test-model",
filepath: "file:///test.ts",
workspaceUris: [],
options: {
maxPromptTokens: 2048,
prefixPercentage: 0.5,
maxSuffixPercentage: 0.5,
experimental_includeClipboard: false,
useRecentlyOpened: false,
experimental_includeRecentlyVisitedRanges: false,
experimental_includeRecentlyEditedRanges: false,
experimental_includeDiff: false,
onlyMyCode: false,
},
} as unknown as HelperVars
}
afterEach(() => {
// reset overridable mocks
templateOverride = (prefix: string, suffix: string) => `${prefix}|${suffix}`
compileFnOverride = undefined
completionOptionsOverride = undefined
stopTokenReturn = ["<STOP>"]
vi.restoreAllMocks()
})
// ---------- Test suite ----------
describe("renderPrompt prefix/suffix selection", () => {
it("uses manuallyPassPrefix when provided", () => {
const helper = makeHelper()
helper.input.manuallyPassPrefix = "MANUAL"
const { prefix, suffix } = renderPrompt({
snippetPayload: emptySnippetPayload,
workspaceDirs: ["file:///workspace"],
helper,
})
expect(suffix).toBe("\n")
expect(prefix.endsWith("MANUAL")).toBe(true)
})
it("falls back to prunedPrefix when no manual prefix", () => {
const helper = makeHelper()
const { prefix } = renderPrompt({
snippetPayload: emptySnippetPayload,
workspaceDirs: ["file:///workspace"],
helper,
})
expect(prefix.includes("PRUNED_PREFIX")).toBe(true)
})
})
describe("template rendering paths", () => {
it("handles function template", () => {
templateOverride = (p: string, s: string, _filepath: string, _reponame: string) => `FUNC:${p}|${s}`
const helper = makeHelper()
const { prompt } = renderPrompt({
snippetPayload: emptySnippetPayload,
workspaceDirs: ["file:///workspace"],
helper,
})
expect(prompt.startsWith("FUNC:")).toBe(true)
expect(prompt.includes("PRUNED_PREFIX")).toBe(true)
})
})
describe("compilePrefixSuffix vs snippet formatting", () => {
it("applies compilePrefixSuffix when provided", () => {
compileFnOverride = (p: string, s: string) => [`COMP_${p}`, `COMP_${s}`]
templateOverride = (prefix: string, suffix: string) => `${prefix}|${suffix}`
const helper = makeHelper()
const { prefix: compiledPrefix } = renderPrompt({
snippetPayload: emptySnippetPayload,
workspaceDirs: ["file:///workspace"],
helper,
})
expect(compiledPrefix.startsWith("COMP_PRUNED_PREFIX")).toBe(true)
})
it("prepends formatted snippets when no compiler present", () => {
const helper = makeHelper()
const { prefix: compiledPrefix } = renderPrompt({
snippetPayload: emptySnippetPayload,
workspaceDirs: ["file:///workspace"],
helper,
})
expect(compiledPrefix.startsWith(`${FORMATTED_SNIPPETS}\n`)).toBe(true)
})
})
describe("renderPromptWithTokenLimit parity & pruning", () => {
it("matches renderPrompt when llm is undefined", () => {
const helper = makeHelper()
const res1 = renderPrompt({
snippetPayload: emptySnippetPayload,
workspaceDirs: ["file:///workspace"],
helper,
})
const res2 = renderPromptWithTokenLimit({
snippetPayload: emptySnippetPayload,
workspaceDirs: ["file:///workspace"],
helper,
llm: undefined,
})
expect(res2).toEqual(res1)
})
it("prunes prefix/suffix to respect small context length", () => {
const longPrefix = "A".repeat(300)
const helper = makeHelper()
;(helper as any).prunedPrefix = longPrefix
const llmStub = {
contextLength: 120,
completionOptions: { maxTokens: 10 },
model: "test-model",
} as any
const { prefix: compiledPrefix } = renderPromptWithTokenLimit({
snippetPayload: emptySnippetPayload,
workspaceDirs: ["file:///workspace"],
helper,
llm: llmStub,
})
expect(compiledPrefix.length).toBeLessThan(120)
})
})
describe("stop-token merging", () => {
it("returns stop tokens from getStopTokens", () => {
stopTokenReturn = ["LANG_STOP", "TEMPLATE_STOP"]
completionOptionsOverride = { stop: ["TEMPLATE_STOP"] }
templateOverride = (prefix: string, suffix: string) => `${prefix}|${suffix}`
const helper = makeHelper()
const { completionOptions } = renderPrompt({
snippetPayload: emptySnippetPayload,
workspaceDirs: ["file:///workspace"],
helper,
})
expect(completionOptions?.stop).toEqual(stopTokenReturn)
})
})
@@ -1,90 +0,0 @@
import { getLastNUriRelativePathParts } from "../../util/uri"
import {
AutocompleteClipboardSnippet,
AutocompleteCodeSnippet,
AutocompleteDiffSnippet,
AutocompleteSnippet,
AutocompleteSnippetType,
AutocompleteStaticSnippet,
} from "../types"
import { HelperVars } from "../util/HelperVars"
const getCommentMark = (helper: HelperVars) => {
return helper.lang.singleLineComment
}
const addCommentMarks = (text: string, helper: HelperVars) => {
const commentMark = getCommentMark(helper)
return text
.trim()
.split("\n")
.map((line) => `${commentMark} ${line}`)
.join("\n")
}
const formatClipboardSnippet = (
snippet: AutocompleteClipboardSnippet,
workspaceDirs: string[],
): AutocompleteCodeSnippet => {
return formatCodeSnippet(
{
filepath: "file:///Untitled.txt",
content: snippet.content,
type: AutocompleteSnippetType.Code,
},
workspaceDirs,
)
}
const formatCodeSnippet = (snippet: AutocompleteCodeSnippet, workspaceDirs: string[]): AutocompleteCodeSnippet => {
return {
...snippet,
content: `Path: ${getLastNUriRelativePathParts(workspaceDirs, snippet.filepath, 2)}\n${snippet.content}`,
}
}
const formatDiffSnippet = (snippet: AutocompleteDiffSnippet): AutocompleteDiffSnippet => {
return snippet
}
const formatStaticSnippet = (snippet: AutocompleteStaticSnippet): AutocompleteStaticSnippet => {
return snippet
}
const commentifySnippet = (helper: HelperVars, snippet: AutocompleteSnippet): AutocompleteSnippet => {
return {
...snippet,
content: addCommentMarks(snippet.content, helper),
}
}
export const formatSnippets = (
helper: HelperVars,
snippets: AutocompleteSnippet[],
workspaceDirs: string[],
): string => {
const currentFilepathComment = addCommentMarks(
getLastNUriRelativePathParts(workspaceDirs, helper.filepath, 2),
helper,
)
return (
snippets
.map((snippet) => {
switch (snippet.type) {
case AutocompleteSnippetType.Code:
return formatCodeSnippet(snippet, workspaceDirs)
case AutocompleteSnippetType.Diff:
return formatDiffSnippet(snippet)
case AutocompleteSnippetType.Clipboard:
return formatClipboardSnippet(snippet, workspaceDirs)
case AutocompleteSnippetType.Static:
return formatStaticSnippet(snippet)
}
})
.map((item) => {
return commentifySnippet(helper, item).content
})
.join("\n") + `\n${currentFilepathComment}`
)
}
@@ -1,28 +0,0 @@
import { CompletionOptions } from "../.."
import { AutocompleteLanguageInfo } from "../constants/AutocompleteLanguageInfo"
// TODO: Do we want to stop completions when reaching a `/src/` string?
const SRC_DIRECTORY = "/src/"
// Starcoder2 tends to output artifacts starting with the letter "t"
const STARCODER2_T_ARTIFACTS = ["t.", "\nt", "<file_sep>"]
const PYTHON_ENCODING = "#- coding: utf-8"
const CODE_BLOCK_END = "```"
// const multilineStops: string[] = [DOUBLE_NEWLINE, WINDOWS_DOUBLE_NEWLINE];
const commonStops = [SRC_DIRECTORY, PYTHON_ENCODING, CODE_BLOCK_END]
export function getStopTokens(
completionOptions: Partial<CompletionOptions> | undefined,
_lang: AutocompleteLanguageInfo,
model: string,
): string[] {
const stopTokens = [
...(completionOptions?.stop || []),
// ...multilineStops,
...commonStops,
...(model.toLowerCase().includes("starcoder2") ? STARCODER2_T_ARTIFACTS : []),
// ...lang.topLevelKeywords.map((word) => `\n${word}`),
]
return stopTokens
}
@@ -1,205 +0,0 @@
import { CompletionOptions } from "../.."
import { HelperVars } from "../util/HelperVars"
import { ILLM } from "../../index.js"
import { DEFAULT_MAX_TOKENS } from "../../llm/constants.js"
import {
countTokens,
getTokenCountingBufferSafety,
pruneLinesFromBottom,
pruneLinesFromTop,
} from "../../llm/countTokens"
import { getUriPathBasename } from "../../util/uri"
import { SnippetPayload } from "../snippets"
import { AutocompleteSnippet } from "../types"
import { AutocompleteTemplate, getTemplateForModel } from "./AutocompleteTemplate"
import { getSnippets } from "./filtering"
import { formatSnippets } from "./formatting"
import { getStopTokens } from "./getStopTokens"
function getTemplate(helper: HelperVars): AutocompleteTemplate {
return getTemplateForModel(helper.modelName)
}
/** Consolidates shared setup between renderPrompt and renderPromptWithTokenLimit. */
function preparePromptContext({
snippetPayload,
workspaceDirs,
helper,
}: {
snippetPayload: SnippetPayload
workspaceDirs: string[]
helper: HelperVars
}): {
prefix: string
suffix: string
reponame: string
template: AutocompleteTemplate["template"]
compilePrefixSuffix: AutocompleteTemplate["compilePrefixSuffix"] | undefined
completionOptions: Partial<CompletionOptions> | undefined
snippets: AutocompleteSnippet[]
} {
// Determine base prefix/suffix, accounting for any manually supplied prefix.
const prefix = helper.input.manuallyPassPrefix || helper.prunedPrefix
let suffix = helper.input.manuallyPassPrefix ? "" : helper.prunedSuffix
if (suffix === "") {
suffix = "\n"
}
const reponame = getUriPathBasename(workspaceDirs[0] ?? "myproject")
const { template, compilePrefixSuffix, completionOptions } = getTemplate(helper)
const snippets = getSnippets(helper, snippetPayload)
return {
prefix,
suffix,
reponame,
template,
compilePrefixSuffix,
completionOptions,
snippets,
}
}
export function renderPrompt({
snippetPayload,
workspaceDirs,
helper,
}: {
snippetPayload: SnippetPayload
workspaceDirs: string[]
helper: HelperVars
}): {
prompt: string
prefix: string
suffix: string
completionOptions: Partial<CompletionOptions> | undefined
} {
const { prefix, suffix, reponame, template, compilePrefixSuffix, completionOptions, snippets } = preparePromptContext(
{ snippetPayload, workspaceDirs, helper },
)
// Delegate prompt construction to buildPrompt to avoid duplication.
const {
prompt,
prefix: compiledPrefix,
suffix: compiledSuffix,
} = buildPrompt(template, compilePrefixSuffix, prefix, suffix, helper, snippets, workspaceDirs, reponame)
const stopTokens = getStopTokens(completionOptions, helper.lang, helper.modelName)
return {
prompt,
prefix: compiledPrefix,
suffix: compiledSuffix,
completionOptions: {
...completionOptions,
stop: stopTokens,
},
}
}
/** Builds the final prompt by applying prefix/suffix compilation or snippet formatting, then rendering the template. */
function buildPrompt(
template: AutocompleteTemplate["template"],
compilePrefixSuffix: AutocompleteTemplate["compilePrefixSuffix"] | undefined,
prefix: string,
suffix: string,
helper: HelperVars,
snippets: AutocompleteSnippet[],
workspaceDirs: string[],
reponame: string,
): { prompt: string; prefix: string; suffix: string } {
if (compilePrefixSuffix) {
;[prefix, suffix] = compilePrefixSuffix(prefix, suffix, helper.filepath, reponame, snippets, helper.workspaceUris)
} else {
const formatted = formatSnippets(helper, snippets, workspaceDirs)
prefix = [formatted, prefix].join("\n")
}
const prompt = template(prefix, suffix, helper.filepath, reponame, helper.lang.name, snippets, helper.workspaceUris)
return { prompt, prefix, suffix }
}
function pruneLength(llm: ILLM, prompt: string): number {
const contextLength = llm.contextLength
const reservedTokens = llm.completionOptions.maxTokens ?? DEFAULT_MAX_TOKENS
const safetyBuffer = getTokenCountingBufferSafety(contextLength)
const maxAllowedPromptTokens = contextLength - reservedTokens - safetyBuffer
const promptTokenCount = countTokens(prompt, llm.model)
return promptTokenCount - maxAllowedPromptTokens
}
export function renderPromptWithTokenLimit({
snippetPayload,
workspaceDirs,
helper,
llm,
}: {
snippetPayload: SnippetPayload
workspaceDirs: string[]
helper: HelperVars
llm: ILLM | undefined
}): {
prompt: string
prefix: string
suffix: string
completionOptions: Partial<CompletionOptions> | undefined
} {
const {
prefix: initialPrefix,
suffix: initialSuffix,
reponame,
template,
compilePrefixSuffix,
completionOptions,
snippets,
} = preparePromptContext({ snippetPayload, workspaceDirs, helper })
// We'll mutate prefix/suffix during pruning, so copy them.
let prefix = initialPrefix
let suffix = initialSuffix
let {
prompt,
prefix: compiledPrefix,
suffix: compiledSuffix,
} = buildPrompt(template, compilePrefixSuffix, prefix, suffix, helper, snippets, workspaceDirs, reponame)
// Truncate prefix and suffix if prompt tokens exceed maxAllowedPromptTokens
if (llm) {
const prune = pruneLength(llm, prompt)
if (prune > 0) {
const tokensToDrop = prune
const prefixTokenCount = countTokens(prefix, helper.modelName)
const suffixTokenCount = countTokens(suffix, helper.modelName)
const totalContextTokens = prefixTokenCount + suffixTokenCount
if (totalContextTokens > 0) {
const dropPrefix = Math.ceil(tokensToDrop * (prefixTokenCount / totalContextTokens))
const dropSuffix = Math.ceil(tokensToDrop - dropPrefix)
const allowedPrefixTokens = Math.max(0, prefixTokenCount - dropPrefix)
const allowedSuffixTokens = Math.max(0, suffixTokenCount - dropSuffix)
prefix = pruneLinesFromTop(prefix, allowedPrefixTokens, helper.modelName)
suffix = pruneLinesFromBottom(suffix, allowedSuffixTokens, helper.modelName)
}
;({
prompt,
prefix: compiledPrefix,
suffix: compiledSuffix,
} = buildPrompt(template, compilePrefixSuffix, prefix, suffix, helper, snippets, workspaceDirs, reponame))
}
}
const stopTokens = getStopTokens(completionOptions, helper.lang, helper.modelName)
return {
prompt,
prefix: compiledPrefix,
suffix: compiledSuffix,
completionOptions: {
...completionOptions,
stop: stopTokens,
},
}
}
@@ -1,32 +0,0 @@
import { randomUUID } from "node:crypto"
export class AutocompleteDebouncer {
private debounceTimeout: NodeJS.Timeout | undefined = undefined
private currentRequestId: string | undefined = undefined
async delayAndShouldDebounce(debounceDelay: number): Promise<boolean> {
// Generate a unique ID for this request
const requestId = randomUUID()
this.currentRequestId = requestId
// Clear any existing timeout
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout)
}
// Create a new promise that resolves after the debounce delay
return new Promise<boolean>((resolve) => {
this.debounceTimeout = setTimeout(() => {
// When the timeout completes, check if this is still the most recent request
const shouldDebounce = this.currentRequestId !== requestId
// If this is the most recent request, it shouldn't be debounced
if (!shouldDebounce) {
this.currentRequestId = undefined
}
resolve(shouldDebounce)
}, debounceDelay)
})
}
}
@@ -1,93 +0,0 @@
import { COUNT_COMPLETION_REJECTED_AFTER } from "../../util/parameters"
import { AutocompleteOutcome } from "./types"
export class AutocompleteLoggingService {
// Key is completionId
private _abortControllers = new Map<string, AbortController>()
private _logRejectionTimeouts = new Map<string, NodeJS.Timeout>()
private _outcomes = new Map<string, AutocompleteOutcome>()
_lastDisplayedCompletion: { id: string; displayedAt: number } | undefined = undefined
public createAbortController(completionId: string): AbortController {
const abortController = new AbortController()
this._abortControllers.set(completionId, abortController)
return abortController
}
public deleteAbortController(completionId: string) {
this._abortControllers.delete(completionId)
}
public cancel() {
this._abortControllers.forEach((abortController) => {
abortController.abort()
})
this._abortControllers.clear()
}
public accept(completionId: string): AutocompleteOutcome | undefined {
if (this._logRejectionTimeouts.has(completionId)) {
clearTimeout(this._logRejectionTimeouts.get(completionId))
this._logRejectionTimeouts.delete(completionId)
}
if (this._outcomes.has(completionId)) {
const outcome = this._outcomes.get(completionId)!
outcome.accepted = true
this.logAutocompleteOutcome(outcome)
this._outcomes.delete(completionId)
return outcome
}
return undefined
}
public cancelRejectionTimeout(completionId: string) {
if (this._logRejectionTimeouts.has(completionId)) {
clearTimeout(this._logRejectionTimeouts.get(completionId)!)
this._logRejectionTimeouts.delete(completionId)
}
if (this._outcomes.has(completionId)) {
this._outcomes.delete(completionId)
}
}
public markDisplayed(completionId: string, outcome: AutocompleteOutcome) {
const logRejectionTimeout = setTimeout(() => {
// Wait 10 seconds, then assume it wasn't accepted
outcome.accepted = false
this.logAutocompleteOutcome(outcome)
this._logRejectionTimeouts.delete(completionId)
}, COUNT_COMPLETION_REJECTED_AFTER)
this._outcomes.set(completionId, outcome)
this._logRejectionTimeouts.set(completionId, logRejectionTimeout)
// If the previously displayed completion is still waiting for rejection,
// and this one is a continuation of that (the outcome.completion is the same modulo prefix)
// then we should cancel the rejection timeout
const previous = this._lastDisplayedCompletion
const now = Date.now()
if (previous && this._logRejectionTimeouts.has(previous.id)) {
const previousOutcome = this._outcomes.get(previous.id)
const c1 = previousOutcome?.completion.split("\n")[0] ?? ""
const c2 = outcome.completion.split("\n")[0]
if (previousOutcome && (c1.endsWith(c2) || c2.endsWith(c1) || c1.startsWith(c2) || c2.startsWith(c1))) {
this.cancelRejectionTimeout(previous.id)
} else if (now - previous.displayedAt < 500) {
// If a completion isn't shown for more than
this.cancelRejectionTimeout(previous.id)
}
}
this._lastDisplayedCompletion = {
id: completionId,
displayedAt: now,
}
}
private logAutocompleteOutcome(outcome: AutocompleteOutcome) {
if (!process.env.VITEST) {
console.log(outcome)
}
}
}
@@ -1,247 +0,0 @@
import { describe, it, expect, beforeEach } from "vitest"
import { AutocompleteLruCacheInMem } from "./AutocompleteLruCacheInMem"
describe("AutoCompleteLruCacheInMem", () => {
let cache: AutocompleteLruCacheInMem
beforeEach(async () => {
cache = await AutocompleteLruCacheInMem.get()
})
describe("basic operations", () => {
it("should store and retrieve a value", async () => {
await cache.put("hello", "world")
const result = await cache.get("hello")
expect(result).toBe("world")
})
it("should return undefined for non-existent key", async () => {
const result = await cache.get("nonexistent")
expect(result).toBeUndefined()
})
it("should update existing value", async () => {
await cache.put("key", "value1")
await cache.put("key", "value2")
const result = await cache.get("key")
expect(result).toBe("value2")
})
})
describe("exact key matching", () => {
it("should match exact key and return value", async () => {
await cache.put("hello", "world")
const result = await cache.get("hello")
expect(result).toBe("world")
})
it("should return undefined when key doesn't match exactly", async () => {
await cache.put("hello", "world")
const result = await cache.get("goodbye")
expect(result).toBeUndefined()
})
it("should return undefined for partial key match", async () => {
await cache.put("hello", "world")
const result = await cache.get("hel")
expect(result).toBeUndefined()
})
it("should be case sensitive", async () => {
await cache.put("Hello", "World")
const result1 = await cache.get("Hello")
const result2 = await cache.get("hello")
expect(result1).toBe("World")
expect(result2).toBeUndefined()
})
})
describe("fuzzy matching", () => {
it("should return completion when prefix extends a cached key", async () => {
// Cache "c" -> "ontinue"
await cache.put("c", "ontinue")
// Query "co" should return "ntinue" (completion minus what we already have)
const result = await cache.get("co")
expect(result).toBe("ntinue")
})
it("should prefer longest matching key", async () => {
// Cache multiple overlapping keys
await cache.put("h", "ello world")
await cache.put("he", "llo world")
await cache.put("hel", "lo world")
// Query "hello" should match "hel" (longest key)
// User typed "hello" = "hel" + "lo", cached completion is "lo world"
// So return " world" (the part not yet typed)
const result = await cache.get("hello")
expect(result).toBe(" world")
})
it("should validate cached completion starts correctly", async () => {
// Cache "c" -> "ontinue"
await cache.put("c", "ontinue")
// Query "cx" doesn't match the completion pattern, should return undefined
const result = await cache.get("cx")
expect(result).toBeUndefined()
})
it("should return exact match if available", async () => {
// Cache both exact and partial keys
await cache.put("co", "mplete")
await cache.put("c", "ontinue")
// Exact match should be preferred
const result = await cache.get("co")
expect(result).toBe("mplete")
})
it("should handle multiple partial matches correctly", async () => {
// Cache overlapping prefixes
await cache.put("fun", "ction")
await cache.put("f", "unction")
// Query "func" should match "fun" (longest) and return "ction"
const result = await cache.get("func")
expect(result).toBe("tion")
})
it("should return undefined when no fuzzy match exists", async () => {
await cache.put("hello", "world")
// "goodbye" doesn't start with "hello"
const result = await cache.get("goodbye")
expect(result).toBeUndefined()
})
it("should handle empty cache for fuzzy matching", async () => {
const result = await cache.get("anyprefix")
expect(result).toBeUndefined()
})
})
describe("LRU eviction", () => {
it("should evict oldest entry when capacity is reached", async () => {
// Create a fresh cache for this test
const testCache = await AutocompleteLruCacheInMem.get()
// Fill cache to capacity (100 entries)
for (let i = 0; i < 100; i++) {
await testCache.put(`key${i}`, `value${i}`)
}
// Add one more entry to trigger eviction
await testCache.put("newkey", "newvalue")
// First entry should be evicted (oldest timestamp)
const result = await testCache.get("key0")
expect(result).toBeUndefined()
// New entry should exist
const newResult = await testCache.get("newkey")
expect(newResult).toBe("newvalue")
})
it("should update timestamp on cache hit", async () => {
// Create a fresh cache for this test
const testCache = await AutocompleteLruCacheInMem.get()
// Fill to capacity
for (let i = 0; i < 100; i++) {
await testCache.put(`key${i}`, `value${i}`)
}
// Access an early entry to refresh its timestamp
const refreshedValue = await testCache.get("key5")
expect(refreshedValue).toBe("value5")
// Add new entries to trigger evictions
await testCache.put("new1", "newvalue1")
await testCache.put("new2", "newvalue2")
// key5 should still exist (refreshed timestamp)
const key5Result = await testCache.get("key5")
expect(key5Result).toBe("value5")
// key0 should be evicted (oldest timestamp, never accessed)
const key0Result = await testCache.get("key0")
expect(key0Result).toBeUndefined()
})
})
describe("edge cases", () => {
it("should handle empty strings", async () => {
const testCache = await AutocompleteLruCacheInMem.get()
await testCache.put("", "empty")
const result = await testCache.get("")
expect(result).toBe("empty")
})
it("should handle very long strings", async () => {
const testCache = await AutocompleteLruCacheInMem.get()
const longString = "a".repeat(10000)
await testCache.put(longString, "completion")
const result = await testCache.get(longString)
expect(result).toBe("completion")
})
it("should handle special characters", async () => {
await cache.put("const x = {", "foo: 'bar'}")
const result = await cache.get("const x = {")
expect(result).toBe("foo: 'bar'}")
})
it("should handle unicode characters", async () => {
await cache.put("emoji 🚀", "rocket")
const result = await cache.get("emoji 🚀")
expect(result).toBe("rocket")
})
})
describe("concurrent operations", () => {
it("should handle concurrent put operations", async () => {
const promises = []
for (let i = 0; i < 10; i++) {
promises.push(cache.put(`concurrent${i}`, `value${i}`))
}
await Promise.all(promises)
// All values should be stored
for (let i = 0; i < 10; i++) {
const result = await cache.get(`concurrent${i}`)
expect(result).toBe(`value${i}`)
}
})
it("should handle concurrent get operations", async () => {
await cache.put("shared", "value")
const promises = []
for (let i = 0; i < 10; i++) {
promises.push(cache.get("shared"))
}
const results = await Promise.all(promises)
// All gets should return the same value
results.forEach((result) => {
expect(result).toBe("value")
})
})
})
describe("multiple cache instances", () => {
it("should create separate cache instances", async () => {
const cache1 = await AutocompleteLruCacheInMem.get()
const cache2 = await AutocompleteLruCacheInMem.get()
await cache1.put("test", "value1")
await cache2.put("test", "value2")
const result1 = await cache1.get("test")
const result2 = await cache2.get("test")
// Each instance should have its own data
expect(result1).toBe("value1")
expect(result2).toBe("value2")
})
})
})
@@ -1,77 +0,0 @@
import { LRUCache } from "lru-cache"
const MAX_PREFIX_LENGTH = 50000
function truncatePrefix(input: string, safety: number = 100): string {
const maxBytes = MAX_PREFIX_LENGTH - safety
let bytes = 0
let startIndex = 0
// Count bytes from the end, keeping the most recent typing
for (let i = input.length - 1; i >= 0; i--) {
bytes += new TextEncoder().encode(input[i]).length
if (bytes > maxBytes) {
startIndex = i + 1
break
}
}
return input.substring(startIndex)
}
export class AutocompleteLruCacheInMem {
private static capacity = 100
private cache: LRUCache<string, string>
private constructor() {
this.cache = new LRUCache<string, string>({
max: AutocompleteLruCacheInMem.capacity,
})
}
static async get(): Promise<AutocompleteLruCacheInMem> {
return new AutocompleteLruCacheInMem()
}
async get(prefix: string): Promise<string | undefined> {
const truncated = truncatePrefix(prefix)
// First try exact match (faster)
const exactMatch = this.cache.get(truncated)
if (exactMatch !== undefined) {
return exactMatch
}
// Then try fuzzy matching - find keys where prefix starts with the key
// If the query is "co" and we have "c" -> "ontinue" in the cache,
// we should return "ntinue" as the completion.
// Have to make sure we take the key with longest length for best match
let bestMatch: { key: string; value: string } | null = null
let longestKeyLength = 0
for (const [key, value] of this.cache.entries()) {
// Check if truncated prefix starts with this key
if (truncated.startsWith(key) && key.length > longestKeyLength) {
bestMatch = { key, value }
longestKeyLength = key.length
}
}
if (bestMatch) {
// Validate that the cached completion is a valid completion for the prefix
if (bestMatch.value.startsWith(truncated.slice(bestMatch.key.length))) {
// Update LRU timestamp for the matched key by accessing it
this.cache.get(bestMatch.key)
// Return the portion of the value that extends beyond the current prefix
return bestMatch.value.slice(truncated.length - bestMatch.key.length)
}
}
return undefined
}
async put(prefix: string, completion: string) {
const truncated = truncatePrefix(prefix)
this.cache.set(truncated, completion)
}
}
@@ -1,63 +0,0 @@
import { describe, expect, it } from "vitest"
import { processTestCase, type CompletionTestCase } from "./completionTestUtils"
describe("processTestCase utility", () => {
it("processes simple console.log completion", () => {
const testCase: CompletionTestCase = {
original: "console.log(|cur||till|)",
completion: '"foo,", bar',
}
expect(processTestCase(testCase)).toEqual({
input: {
lastLineOfCompletionText: '"foo,", bar',
currentText: ")",
cursorPosition: "console.log(".length,
},
expectedResult: {
completionText: '"foo,", bar',
},
})
})
it("processes simple console.log completion with overwriting", () => {
const testCase: CompletionTestCase = {
original: "console.log(|cur|)|till|",
completion: '"foo,", bar);',
}
expect(processTestCase(testCase)).toEqual({
input: {
lastLineOfCompletionText: '"foo,", bar);',
currentText: ")",
cursorPosition: "console.log(".length,
},
expectedResult: {
completionText: '"foo,", bar);',
range: {
start: "console.log(".length,
end: "console.log()".length,
},
},
})
})
it("partially applying completion", () => {
const testCase: CompletionTestCase = {
original: '|cur||till|fetch("https://example.com");',
completion: 'await fetch("https://example.com");',
appliedCompletion: "await ",
}
expect(processTestCase(testCase)).toEqual({
input: {
lastLineOfCompletionText: 'await fetch("https://example.com");',
currentText: 'fetch("https://example.com");',
cursorPosition: 0,
},
expectedResult: {
completionText: "await ",
},
})
})
})
@@ -1,94 +0,0 @@
export interface CompletionTestCase {
original: string // Text with |cur| and |till| markers
completion: string // Text to insert/overwrite
appliedCompletion?: string | null
cursorMarker?: string
tillMarker?: string
}
interface ProcessedTestCase {
input: {
lastLineOfCompletionText: string
currentText: string
cursorPosition: number
}
expectedResult: {
completionText: string
range?: {
start: number
end: number
}
}
}
/**
* Transforms human-readable test case into input and expected results.
*
* - `original`: Your original text with |cur| marking where the cursor is before completion,
* and |till| marking where the cursor should be after accepting completion (and this is
* the end of the actual applied completion)
* - `completion`: LLM completion output
* - `appliedCompletion` (optional): part of the LLM completion output that is actually applied
* (written between |cur| and |till| in the original)
*
* For example, you have this line:
*
* console.log("<cursor here>");
*
* and expect it to be completed this way:
*
* console.log("foo: ", bar<cursor here>);
*
* with your completion coming from LLM being: `'foo: ", bar<cursor here>);'`
*
* Your input to this function should be:
* - original: `'console.log("|cur|"|till|);'`
* - completion: `'foo: ", bar);'`
* - appliedCompletion: `'foo: ", bar'`
*
* Output: input and expected output of {@link core/autocomplete/util/processSingleLineCompletion/processSingleLineCompletion|processSingleLineCompletion()}
*
*/
export function processTestCase({
original,
completion,
appliedCompletion = null,
cursorMarker = "|cur|",
tillMarker = "|till|",
}: CompletionTestCase): ProcessedTestCase {
// Validate cursor marker
if (!original.includes(cursorMarker)) {
throw new Error("Cursor marker not found in original text")
}
const cursorPos = original.indexOf(cursorMarker)
original = original.replace(cursorMarker, "")
let tillPos = original.indexOf(tillMarker)
if (tillPos < 0) {
tillPos = cursorPos
} else {
original = original.replace(tillMarker, "")
}
// Calculate currentText based on what's between cursor and till marker
const currentText = original.substring(cursorPos)
return {
input: {
lastLineOfCompletionText: completion,
currentText,
cursorPosition: cursorPos,
},
expectedResult: {
completionText: appliedCompletion || completion,
range:
cursorPos === tillPos
? undefined
: {
start: cursorPos,
end: tillPos,
},
},
}
}
@@ -1,96 +0,0 @@
import { describe, expect, it } from "vitest"
import { processTestCase } from "./completionTestUtils"
import { processSingleLineCompletion } from "./processSingleLineCompletion"
describe("processSingleLineCompletion", () => {
it("should handle simple end of line completion", () => {
const testCase = processTestCase({
original: "console.log(|cur|",
completion: '"Hello, world!")',
})
const result = processSingleLineCompletion(
testCase.input.lastLineOfCompletionText,
testCase.input.currentText,
testCase.input.cursorPosition,
)
expect(result).toEqual(testCase.expectedResult)
})
it("should handle midline insert repeating the end of line", () => {
const testCase = processTestCase({
original: "console.log(|cur|);|till|",
completion: '"Hello, world!");',
})
const result = processSingleLineCompletion(
testCase.input.lastLineOfCompletionText,
testCase.input.currentText,
testCase.input.cursorPosition,
)
expect(result).toEqual(testCase.expectedResult)
})
it("should handle midline insert repeating the end of line plus adding a semicolon", () => {
const testCase = processTestCase({
original: "console.log(|cur|)|till|",
completion: '"Hello, world!");',
})
const result = processSingleLineCompletion(
testCase.input.lastLineOfCompletionText,
testCase.input.currentText,
testCase.input.cursorPosition,
)
expect(result).toEqual(testCase.expectedResult)
})
it("should handle simple midline insert", () => {
const testCase = processTestCase({
original: "console.log(|cur|)",
completion: '"Hello, world!"',
})
const result = processSingleLineCompletion(
testCase.input.lastLineOfCompletionText,
testCase.input.currentText,
testCase.input.cursorPosition,
)
expect(result).toEqual(testCase.expectedResult)
})
it("should handle complex dif with addition in the beginning", () => {
const testCase = processTestCase({
original: 'console.log(|cur||till|, "param1", )', // TODO
completion: '"Hello world!", "param1", param1);',
appliedCompletion: '"Hello world!"',
})
const result = processSingleLineCompletion(
testCase.input.lastLineOfCompletionText,
testCase.input.currentText,
testCase.input.cursorPosition,
)
expect(result).toEqual(testCase.expectedResult)
})
it("should handle simple insertion even with random equality", () => {
const testCase = processTestCase({
original: 'print(f"Foobar length: |cur||till|")',
completion: "{len(foobar)}",
})
const result = processSingleLineCompletion(
testCase.input.lastLineOfCompletionText,
testCase.input.currentText,
testCase.input.cursorPosition,
)
expect(result).toEqual(testCase.expectedResult)
})
})
@@ -1,80 +0,0 @@
import * as Diff from "diff"
interface SingleLineCompletionResult {
completionText: string
range?: {
start: number
end: number
}
}
interface DiffType {
count?: number
added?: boolean
removed?: boolean
value: string
}
function diffPatternMatches(diffs: DiffType[], pattern: DiffPartType[]): boolean {
if (diffs.length !== pattern.length) {
return false
}
for (let i = 0; i < diffs.length; i++) {
const diff = diffs[i]
const diffPartType: DiffPartType = !diff.added && !diff.removed ? "=" : diff.added ? "+" : "-"
if (diffPartType !== pattern[i]) {
return false
}
}
return true
}
type DiffPartType = "+" | "-" | "="
export function processSingleLineCompletion(
lastLineOfCompletionText: string,
currentText: string,
cursorPosition: number,
): SingleLineCompletionResult | undefined {
const diffs: DiffType[] = Diff.diffWords(currentText, lastLineOfCompletionText)
if (diffPatternMatches(diffs, ["+"])) {
// Just insert, we're already at the end of the line
return {
completionText: lastLineOfCompletionText,
}
}
if (diffPatternMatches(diffs, ["+", "="]) || diffPatternMatches(diffs, ["+", "=", "+"])) {
// The model repeated the text after the cursor to the end of the line
return {
completionText: lastLineOfCompletionText,
range: {
start: cursorPosition,
end: currentText.length + cursorPosition,
},
}
}
if (diffPatternMatches(diffs, ["+", "-"]) || diffPatternMatches(diffs, ["-", "+"])) {
// We are midline and the model just inserted without repeating to the end of the line
return {
completionText: lastLineOfCompletionText,
}
}
// For any other diff pattern, just use the first added part if available
if (diffs[0]?.added) {
return {
completionText: diffs[0].value,
}
}
// Default case: treat as simple insertion
return {
completionText: lastLineOfCompletionText,
}
}
@@ -1,5 +1,5 @@
import { Position, Range, RangeInFile, TabAutocompleteOptions } from "../.."
import { AutocompleteCodeSnippet } from "../types"
import type { Position, Range, RangeInFile } from "../.."
import type { AutocompleteCodeSnippet } from "../types"
export type RecentlyEditedRange = RangeInFile & {
timestamp: number
@@ -24,24 +24,3 @@ export interface AutocompleteInput {
}
injectDetails?: string
}
export interface AutocompleteOutcome extends TabAutocompleteOptions {
accepted?: boolean
time: number
prefix: string
suffix: string
prompt: string
completion: string
modelProvider: string
modelName: string
completionOptions: any
cacheHit: boolean
numLines: number
filepath: string
gitRepo?: string
completionId: string
uniqueId: string
timestamp: string
enabledStaticContextualization?: boolean
profileType?: "local" | "platform" | "control-plane"
}
@@ -1,634 +0,0 @@
import { describe, expect, test } from "vitest"
import { dedent } from "../util"
import { myersCharDiff, myersDiff } from "./myers"
describe("Test myers diff function", () => {
test("should ...", () => {
const linesA = dedent`
A
B
C
D
E
`
const linesB = dedent`
A
B
C'
D'
E
`
const diffLines = myersDiff(linesA, linesB)
expect(diffLines).toEqual([
{ type: "same", line: "A" },
{ type: "same", line: "B" },
{ type: "old", line: "C" },
{ type: "old", line: "D" },
{ type: "new", line: "C'" },
{ type: "new", line: "D'" },
{ type: "same", line: "E" },
])
})
test("should ignore newline differences at end", () => {
const linesA = "A\nB\nC\n"
const linesB = "A\nB\nC"
const diffLines = myersDiff(linesA, linesB)
expect(diffLines).toEqual([
{ type: "same", line: "A" },
{ type: "same", line: "B" },
{ type: "same", line: "C" },
])
})
test("should ignore single-line whitespace-only differences", () => {
const linesA = "A\n B\nC\n"
const linesB = "A\nB\nC"
const diffLines = myersDiff(linesA, linesB)
expect(diffLines).toEqual([
{ type: "same", line: "A" },
{ type: "same", line: " B" },
{ type: "same", line: "C" },
])
})
})
describe("Test myersCharDiff function on the same line", () => {
test("should differentiate character changes", () => {
const oldContent = "hello world"
const newContent = "hello earth"
const diffChars = myersCharDiff(oldContent, newContent)
expect(diffChars).toEqual([
{
type: "same",
char: "hello ",
oldIndex: 0,
newIndex: 0,
oldCharIndexInLine: 0,
newCharIndexInLine: 0,
oldLineIndex: 0,
newLineIndex: 0,
},
{
type: "old",
char: "wo",
oldIndex: 6,
oldCharIndexInLine: 6,
oldLineIndex: 0,
},
{
type: "new",
char: "ea",
newIndex: 6,
newCharIndexInLine: 6,
newLineIndex: 0,
},
{
type: "same",
char: "r",
oldIndex: 8,
newIndex: 8,
oldCharIndexInLine: 8,
newCharIndexInLine: 8,
oldLineIndex: 0,
newLineIndex: 0,
},
{
type: "old",
char: "ld",
oldIndex: 9,
oldCharIndexInLine: 9,
oldLineIndex: 0,
},
{
type: "new",
char: "th",
newIndex: 9,
newCharIndexInLine: 9,
newLineIndex: 0,
},
])
})
test("should handle insertions", () => {
const oldContent = "abc"
const newContent = "abxyzc"
const diffChars = myersCharDiff(oldContent, newContent)
expect(diffChars).toEqual([
{
type: "same",
char: "ab",
oldIndex: 0,
newIndex: 0,
oldCharIndexInLine: 0,
newCharIndexInLine: 0,
oldLineIndex: 0,
newLineIndex: 0,
},
{
type: "new",
char: "xyz",
newIndex: 2,
newCharIndexInLine: 2,
newLineIndex: 0,
},
{
type: "same",
char: "c",
oldIndex: 2,
newIndex: 5,
oldCharIndexInLine: 2,
newCharIndexInLine: 5,
oldLineIndex: 0,
newLineIndex: 0,
},
])
})
test("should handle deletions", () => {
const oldContent = "abxyzc"
const newContent = "abc"
const diffChars = myersCharDiff(oldContent, newContent)
expect(diffChars).toEqual([
{
type: "same",
char: "ab",
oldIndex: 0,
newIndex: 0,
oldCharIndexInLine: 0,
newCharIndexInLine: 0,
oldLineIndex: 0,
newLineIndex: 0,
},
{
type: "old",
char: "xyz",
oldIndex: 2,
oldCharIndexInLine: 2,
oldLineIndex: 0,
},
{
type: "same",
char: "c",
oldIndex: 5,
newIndex: 2,
oldCharIndexInLine: 5,
newCharIndexInLine: 2,
oldLineIndex: 0,
newLineIndex: 0,
},
])
})
test("should handle empty strings", () => {
const oldContent = ""
const newContent = "abc"
const diffChars = myersCharDiff(oldContent, newContent)
expect(diffChars).toEqual([
{
type: "new",
char: "abc",
newIndex: 0,
newCharIndexInLine: 0,
newLineIndex: 0,
},
])
})
test("should handle identical strings", () => {
const content = "no changes here"
const diffChars = myersCharDiff(content, content)
expect(diffChars).toEqual([
{
type: "same",
char: "no changes here",
oldIndex: 0,
newIndex: 0,
oldCharIndexInLine: 0,
newCharIndexInLine: 0,
oldLineIndex: 0,
newLineIndex: 0,
},
])
})
test("should handle whitespace changes", () => {
const oldContent = "hello world"
const newContent = "hello world"
const diffChars = myersCharDiff(oldContent, newContent)
expect(diffChars).toEqual([
{
type: "same",
char: "hello ",
oldIndex: 0,
newIndex: 0,
oldCharIndexInLine: 0,
newCharIndexInLine: 0,
oldLineIndex: 0,
newLineIndex: 0,
},
{
type: "new",
char: " ",
newIndex: 6,
newCharIndexInLine: 6,
newLineIndex: 0,
},
{
type: "same",
char: "world",
oldIndex: 6,
newIndex: 7,
oldCharIndexInLine: 6,
newCharIndexInLine: 7,
oldLineIndex: 0,
newLineIndex: 0,
},
])
})
test("should handle complex changes", () => {
const oldContent = "The quick brown fox jumps over the lazy dog"
const newContent = "The fast brown fox leaps over the sleeping dog"
const diffChars = myersCharDiff(oldContent, newContent)
expect(diffChars).toEqual([
{
type: "same",
char: "The ",
oldIndex: 0,
newIndex: 0,
oldCharIndexInLine: 0,
newCharIndexInLine: 0,
oldLineIndex: 0,
newLineIndex: 0,
},
{
type: "old",
char: "quick",
oldIndex: 4,
oldCharIndexInLine: 4,
oldLineIndex: 0,
},
{
type: "new",
char: "fast",
newIndex: 4,
newCharIndexInLine: 4,
newLineIndex: 0,
},
{
type: "same",
char: " brown fox ",
oldIndex: 9,
newIndex: 8,
oldCharIndexInLine: 9,
newCharIndexInLine: 8,
oldLineIndex: 0,
newLineIndex: 0,
},
{
type: "old",
char: "jum",
oldIndex: 20,
oldCharIndexInLine: 20,
oldLineIndex: 0,
},
{
type: "new",
char: "lea",
newIndex: 19,
newCharIndexInLine: 19,
newLineIndex: 0,
},
{
type: "same",
char: "ps over the ",
oldIndex: 23,
newIndex: 22,
oldCharIndexInLine: 23,
newCharIndexInLine: 22,
oldLineIndex: 0,
newLineIndex: 0,
},
{
type: "new",
char: "s",
newIndex: 34,
newCharIndexInLine: 34,
newLineIndex: 0,
},
{
type: "same",
char: "l",
oldIndex: 35,
newIndex: 35,
oldCharIndexInLine: 35,
newCharIndexInLine: 35,
oldLineIndex: 0,
newLineIndex: 0,
},
{
type: "old",
char: "azy",
oldIndex: 36,
oldCharIndexInLine: 36,
oldLineIndex: 0,
},
{
type: "new",
char: "eeping",
newIndex: 36,
newCharIndexInLine: 36,
newLineIndex: 0,
},
{
type: "same",
char: " dog",
oldIndex: 39,
newIndex: 42,
oldCharIndexInLine: 39,
newCharIndexInLine: 42,
oldLineIndex: 0,
newLineIndex: 0,
},
])
})
})
describe("Test myersCharDiff function on different lines", () => {
test("should track line indices for multi-line changes", () => {
const oldContent = ["Line one", "Line two", "Line three"].join("\n")
const newContent = ["Line one", "Modified line", "Line three"].join("\n")
const diffChars = myersCharDiff(oldContent, newContent)
expect(diffChars).toEqual([
{
type: "same",
char: "Line one",
oldIndex: 0,
newIndex: 0,
oldCharIndexInLine: 0,
newCharIndexInLine: 0,
oldLineIndex: 0,
newLineIndex: 0,
},
{
type: "same",
char: "\n",
oldIndex: 8,
newIndex: 8,
oldCharIndexInLine: 8,
newCharIndexInLine: 8,
oldLineIndex: 0,
newLineIndex: 0,
},
{
type: "old",
char: "L",
oldIndex: 9,
oldCharIndexInLine: 0,
oldLineIndex: 1,
},
{
type: "new",
char: "Mod",
newIndex: 9,
newCharIndexInLine: 0,
newLineIndex: 1,
},
{
char: "i",
newCharIndexInLine: 3,
newIndex: 12,
newLineIndex: 1,
oldCharIndexInLine: 1,
oldIndex: 10,
oldLineIndex: 1,
type: "same",
},
{
char: "n",
oldCharIndexInLine: 2,
oldIndex: 11,
oldLineIndex: 1,
type: "old",
},
{
char: "fi",
newCharIndexInLine: 4,
newIndex: 13,
newLineIndex: 1,
type: "new",
},
{
char: "e",
newCharIndexInLine: 6,
newIndex: 15,
newLineIndex: 1,
oldCharIndexInLine: 3,
oldIndex: 12,
oldLineIndex: 1,
type: "same",
},
{
char: "d",
newCharIndexInLine: 7,
newIndex: 16,
newLineIndex: 1,
type: "new",
},
{
char: " ",
newCharIndexInLine: 8,
newIndex: 17,
newLineIndex: 1,
oldCharIndexInLine: 4,
oldIndex: 13,
oldLineIndex: 1,
type: "same",
},
{
type: "old",
char: "two",
oldCharIndexInLine: 5,
oldIndex: 14,
oldLineIndex: 1,
},
{
type: "new",
char: "line",
newCharIndexInLine: 9,
newIndex: 18,
newLineIndex: 1,
},
{
type: "same",
char: "\n",
oldIndex: 17,
oldCharIndexInLine: 8,
oldLineIndex: 1,
newIndex: 22,
newCharIndexInLine: 13,
newLineIndex: 1,
},
{
type: "same",
char: "Line three",
oldIndex: 18,
newIndex: 23,
oldCharIndexInLine: 0,
newCharIndexInLine: 0,
oldLineIndex: 2,
newLineIndex: 2,
},
])
})
test("should track line indices when adding new lines", () => {
const oldContent = ["First line", "Last line"].join("\n")
const newContent = ["First line", "Middle line", "Another middle", "Last line"].join("\n")
const diffChars = myersCharDiff(oldContent, newContent)
expect(diffChars).toEqual([
{
type: "same",
char: "First line",
oldIndex: 0,
newIndex: 0,
oldCharIndexInLine: 0,
newCharIndexInLine: 0,
oldLineIndex: 0,
newLineIndex: 0,
},
{
type: "same",
char: "\n",
oldIndex: 10,
newIndex: 10,
oldCharIndexInLine: 10,
newCharIndexInLine: 10,
oldLineIndex: 0,
newLineIndex: 0,
},
{
type: "new",
char: "Middle line",
newIndex: 11,
newCharIndexInLine: 0,
newLineIndex: 1,
},
{
type: "new",
char: "\n",
newIndex: 22,
newCharIndexInLine: 11,
newLineIndex: 1,
},
{
type: "new",
char: "Another middle",
newCharIndexInLine: 0,
newIndex: 23,
newLineIndex: 2,
},
{
type: "new",
char: "\n",
newCharIndexInLine: 14,
newIndex: 37,
newLineIndex: 2,
},
{
type: "same",
char: "Last line",
oldIndex: 11,
oldCharIndexInLine: 0,
oldLineIndex: 1,
newIndex: 38,
newCharIndexInLine: 0,
newLineIndex: 3,
},
])
})
test("should track line indices when removing lines", () => {
const oldContent = ["Start", "Line to remove", "Another to remove", "End"].join("\n")
const newContent = ["Start", "End"].join("\n")
const diffChars = myersCharDiff(oldContent, newContent)
expect(diffChars).toEqual([
{
type: "same",
char: "Start",
oldIndex: 0,
newIndex: 0,
oldCharIndexInLine: 0,
newCharIndexInLine: 0,
oldLineIndex: 0,
newLineIndex: 0,
},
{
type: "same",
char: "\n",
oldIndex: 5,
newIndex: 5,
oldCharIndexInLine: 5,
newCharIndexInLine: 5,
oldLineIndex: 0,
newLineIndex: 0,
},
{
type: "old",
char: "Line to remove",
oldIndex: 6,
oldCharIndexInLine: 0,
oldLineIndex: 1,
},
{
type: "old",
char: "\n",
oldCharIndexInLine: 14,
oldIndex: 20,
oldLineIndex: 1,
},
{
type: "old",
char: "Another to remove",
oldCharIndexInLine: 0,
oldIndex: 21,
oldLineIndex: 2,
},
{
type: "old",
char: "\n",
oldCharIndexInLine: 17,
oldIndex: 38,
oldLineIndex: 2,
},
{
type: "same",
char: "End",
oldIndex: 39,
newIndex: 6,
oldCharIndexInLine: 0,
newCharIndexInLine: 0,
oldLineIndex: 3,
newLineIndex: 1,
},
])
})
})
@@ -1,204 +0,0 @@
import { diffChars, diffLines, type Change } from "diff"
import { DiffChar, DiffLine } from ".."
function convertMyersChangeToDiffLines(change: Change): DiffLine[] {
const type: DiffLine["type"] = change.added ? "new" : change.removed ? "old" : "same"
const lines = change.value.split("\n")
// Ignore the \n at the end of the final line, if there is one
if (lines[lines.length - 1] === "") {
lines.pop()
}
return lines.map((line) => ({ type, line }))
}
// The interpretation of lines in oldContent and newContent is the same as jsdiff
// Lines are separated by \n, with the exception that a trailing \n does *not*
// represent an empty line.
//
// The default for jsdiff is that "foo" and "foo\n" are *different* single-line
// contents, but we can't represent that: to avoid a diff
// [ { type: "old", line: "foo" }, { type: "new", line: "foo" } ], we
// pass ignoreNewlineAtEof: true.
export function myersDiff(oldContent: string, newContent: string): DiffLine[] {
const theirFormat = diffLines(oldContent, newContent, {
ignoreNewlineAtEof: true,
})
const ourFormat = theirFormat.flatMap(convertMyersChangeToDiffLines)
// Combine consecutive old/new pairs that are identical after trimming
for (let i = 0; i < ourFormat.length - 1; i++) {
if (
ourFormat[i]?.type === "old" &&
ourFormat[i + 1]?.type === "new" &&
ourFormat[i].line.trim() === ourFormat[i + 1].line.trim()
) {
ourFormat[i] = { type: "same", line: ourFormat[i].line }
ourFormat.splice(i + 1, 1)
}
}
// Remove trailing empty old lines
while (
ourFormat.length > 0 &&
ourFormat[ourFormat.length - 1].type === "old" &&
ourFormat[ourFormat.length - 1].line === ""
) {
ourFormat.pop()
}
return ourFormat
}
export function myersCharDiff(oldContent: string, newContent: string): DiffChar[] {
// Process the content character by character.
// We will handle newlines separately,
// because diffChars does not have an option to ignore eol newlines.
const theirFormat = diffChars(oldContent, newContent)
// Track indices as we process the diff.
let oldIndex = 0
let newIndex = 0
let oldLineIndex = 0
let newLineIndex = 0
let oldCharIndexInLine = 0
let newCharIndexInLine = 0
const result: DiffChar[] = []
for (const change of theirFormat) {
// Split the change value by newlines to handle them separately.
if (change.value.includes("\n")) {
const parts = change.value.split(/(\n)/g) // This keeps the newlines as separate entries.
for (let i = 0; i < parts.length; i++) {
const part = parts[i]
if (part === "") continue
if (part === "\n") {
// Handle newline.
if (change.added) {
result.push({
type: "new",
char: part,
newIndex: newIndex,
newLineIndex: newLineIndex,
newCharIndexInLine: newCharIndexInLine,
})
newIndex += part.length
newLineIndex++
newCharIndexInLine = 0 // Reset when moving to a new line.
} else if (change.removed) {
result.push({
type: "old",
char: part,
oldIndex: oldIndex,
oldLineIndex: oldLineIndex,
oldCharIndexInLine: oldCharIndexInLine,
})
oldIndex += part.length
oldLineIndex++
oldCharIndexInLine = 0 // Reset when moving to a new line.
} else {
result.push({
type: "same",
char: part,
oldIndex: oldIndex,
newIndex: newIndex,
oldLineIndex: oldLineIndex,
newLineIndex: newLineIndex,
oldCharIndexInLine: oldCharIndexInLine,
newCharIndexInLine: newCharIndexInLine,
})
oldIndex += part.length
newIndex += part.length
oldLineIndex++
newLineIndex++
oldCharIndexInLine = 0
newCharIndexInLine = 0
}
} else {
// Handle regular text.
if (change.added) {
result.push({
type: "new",
char: part,
newIndex: newIndex,
newLineIndex: newLineIndex,
newCharIndexInLine: newCharIndexInLine,
})
newIndex += part.length
newCharIndexInLine += part.length
} else if (change.removed) {
result.push({
type: "old",
char: part,
oldIndex: oldIndex,
oldLineIndex: oldLineIndex,
oldCharIndexInLine: oldCharIndexInLine,
})
oldIndex += part.length
oldCharIndexInLine += part.length
} else {
result.push({
type: "same",
char: part,
oldIndex: oldIndex,
newIndex: newIndex,
oldLineIndex: oldLineIndex,
newLineIndex: newLineIndex,
oldCharIndexInLine: oldCharIndexInLine,
newCharIndexInLine: newCharIndexInLine,
})
oldIndex += part.length
newIndex += part.length
oldCharIndexInLine += part.length
newCharIndexInLine += part.length
}
}
}
} else {
// No newlines, handle as a simple change.
if (change.added) {
result.push({
type: "new",
char: change.value,
newIndex: newIndex,
newLineIndex: newLineIndex,
newCharIndexInLine: newCharIndexInLine,
})
newIndex += change.value.length
newCharIndexInLine += change.value.length
} else if (change.removed) {
result.push({
type: "old",
char: change.value,
oldIndex: oldIndex,
oldLineIndex: oldLineIndex,
oldCharIndexInLine: oldCharIndexInLine,
})
oldIndex += change.value.length
oldCharIndexInLine += change.value.length
} else {
result.push({
type: "same",
char: change.value,
oldIndex: oldIndex,
newIndex: newIndex,
oldLineIndex: oldLineIndex,
newLineIndex: newLineIndex,
oldCharIndexInLine: oldCharIndexInLine,
newCharIndexInLine: newCharIndexInLine,
})
oldIndex += change.value.length
newIndex += change.value.length
oldCharIndexInLine += change.value.length
newCharIndexInLine += change.value.length
}
}
}
return result
}
@@ -1,317 +0,0 @@
import fs from "node:fs"
import path from "node:path"
import { describe, expect, test } from "vitest"
// @ts-expect-error no typings available
import { changed, diff as myersDiff } from "myers-diff"
import { streamDiff } from "../diff/streamDiff.js"
import { DiffLine, DiffType } from "../index.js"
import { generateLines } from "./util.js"
// "modification" is an extra type used to represent an "old" + "new" diff line
type MyersDiffTypes = Extract<DiffType, "new" | "old"> | "modification"
const UNIFIED_DIFF_SYMBOLS = {
same: "",
new: "+",
old: "-",
}
async function collectDiffs(
oldLines: string[],
newLines: string[],
): Promise<{ streamDiffs: DiffLine[]; myersDiffs: any }> {
const streamDiffs: DiffLine[] = []
for await (const diffLine of streamDiff(oldLines, generateLines(newLines))) {
streamDiffs.push(diffLine)
}
const myersDiffs = myersDiff(oldLines.join("\n"), newLines.join("\n"))
return { streamDiffs, myersDiffs }
}
function getMyersDiffType(diff: any): MyersDiffTypes | undefined {
if (changed(diff.rhs) && !changed(diff.lhs)) {
return "new"
}
if (!changed(diff.rhs) && changed(diff.lhs)) {
return "old"
}
if (changed(diff.rhs) && changed(diff.lhs)) {
return "modification"
}
return undefined
}
function displayDiff(diff: DiffLine[]) {
return diff.map(({ type, line }) => `${UNIFIED_DIFF_SYMBOLS[type]} ${line}`).join("\n")
}
async function expectDiff(file: string) {
const testFilePath = path.join(__dirname, "test-examples", file + ".diff")
const testFileContents = fs.readFileSync(testFilePath, "utf-8")
const normalized = testFileContents.replace(/\r\n/g, "\n")
const [oldText, newText, expectedDiff] = normalized.split("\n---\n").map((s) => s.replace(/^\n+/, "").trimEnd())
const oldLines = oldText.split("\n")
const newLines = newText.split("\n")
const { streamDiffs } = await collectDiffs(oldLines, newLines)
const displayedDiff = displayDiff(streamDiffs)
if (!expectedDiff || expectedDiff.trim() === "") {
console.log("Expected diff was empty. Writing computed diff to the test file")
// Persist with LF to keep fixtures stable cross-platform
fs.writeFileSync(testFilePath, `${oldText}\n\n---\n\n${newText}\n\n---\n\n${displayedDiff}`)
throw new Error("Expected diff is empty")
}
expect(displayedDiff).toEqual(expectedDiff)
}
// We use a longer `)` string here to not get
// caught by the fuzzy matcher
describe("streamDiff(", () => {
test("no changes", async () => {
const oldLines = ["first item", "second arg", "third param"]
const newLines = ["first item", "second arg", "third param"]
const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)
expect(streamDiffs).toEqual([
{ type: "same", line: "first item" },
{ type: "same", line: "second arg" },
{ type: "same", line: "third param" },
])
expect(myersDiffs).toEqual([])
})
test("add new line", async () => {
const oldLines = ["first item", "second arg"]
const newLines = ["first item", "second arg", "third param"]
const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)
expect(streamDiffs).toEqual([
{ type: "same", line: "first item" },
{ type: "same", line: "second arg" },
{ type: "new", line: "third param" },
])
expect(myersDiffs.length).toEqual(1)
expect(getMyersDiffType(myersDiffs[0])).toBe("new")
})
test("remove line", async () => {
const oldLines = ["first item", "second arg", "third param"]
const newLines = ["first item", "third param"]
const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)
expect(streamDiffs).toEqual([
{ type: "same", line: "first item" },
{ type: "old", line: "second arg" },
{ type: "same", line: "third param" },
])
expect(myersDiffs.length).toEqual(1)
expect(getMyersDiffType(myersDiffs[0])).toBe("old")
})
test("modify line", async () => {
const oldLines = ["first item", "second arg", "third param"]
const newLines = ["first item", "modified second arg", "third param"]
const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)
expect(streamDiffs).toEqual([
{ type: "same", line: "first item" },
{ type: "old", line: "second arg" },
{ type: "new", line: "modified second arg" },
{ type: "same", line: "third param" },
])
expect(myersDiffs.length).toEqual(1)
expect(getMyersDiffType(myersDiffs[0])).toBe("modification")
})
test("add multiple lines", async () => {
const oldLines = ["first item", "fourth val"]
const newLines = ["first item", "second arg", "third param", "fourth val"]
const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)
expect(streamDiffs).toEqual([
{ type: "same", line: "first item" },
{ type: "new", line: "second arg" },
{ type: "new", line: "third param" },
{ type: "same", line: "fourth val" },
])
// Multi-line addition
expect(myersDiffs[0].rhs.add).toEqual(2)
expect(getMyersDiffType(myersDiffs[0])).toBe("new")
})
test("remove multiple lines", async () => {
const oldLines = ["first item", "second arg", "third param", "fourth val"]
const newLines = ["first item", "fourth val"]
const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)
expect(streamDiffs).toEqual([
{ type: "same", line: "first item" },
{ type: "old", line: "second arg" },
{ type: "old", line: "third param" },
{ type: "same", line: "fourth val" },
])
// Multi-line deletion
expect(myersDiffs[0].lhs.del).toEqual(2)
expect(getMyersDiffType(myersDiffs[0])).toBe("old")
})
test("empty old lines", async () => {
const oldLines: string[] = []
const newLines = ["first item", "second arg"]
const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)
expect(streamDiffs).toEqual([
{ type: "new", line: "first item" },
{ type: "new", line: "second arg" },
])
// Multi-line addition
expect(myersDiffs[0].rhs.add).toEqual(2)
expect(getMyersDiffType(myersDiffs[0])).toBe("new")
})
test("empty new lines", async () => {
const oldLines = ["first item", "second arg"]
const newLines: string[] = []
const { streamDiffs, myersDiffs } = await collectDiffs(oldLines, newLines)
expect(streamDiffs).toEqual([
{ type: "old", line: "first item" },
{ type: "old", line: "second arg" },
])
// Multi-line deletion
expect(myersDiffs[0].lhs.del).toEqual(2)
expect(getMyersDiffType(myersDiffs[0])).toBe("old")
})
test("tabs vs. spaces differences are ignored", async () => {
await expectDiff("fastapi-tabs-vs-spaces.py")
})
test("trailing whitespaces should match ", async () => {
const oldLines = ["first item ", "second arg ", "third param "]
const newLines = ["first item", "second arg", "third param "]
const { streamDiffs } = await collectDiffs(oldLines, newLines)
expect(streamDiffs).toEqual([
{ type: "same", line: "first item " },
{ type: "same", line: "second arg " },
{ type: "same", line: "third param " },
])
})
//indentation and whitespace handling
test.each([false, true])(
"ignores indentation changes for sufficiently long lines (trailingWhitespace: %s)",
async (trailingWhitespace) => {
let oldLines = [" short", " middle", " a long enough line", " short2", "indented line", "final line"]
const newLines = ["short", "middle", "a long enough line", "short2", " indented line", "final line"]
if (trailingWhitespace) {
oldLines = oldLines.map((line) => line + " ")
}
const { streamDiffs } = await collectDiffs(oldLines, newLines)
const expected = trailingWhitespace
? [
{ type: "old", line: " short " },
{ type: "new", line: "short" },
{ type: "old", line: " middle " },
{ type: "new", line: "middle" },
{ type: "same", line: " a long enough line " },
{ type: "same", line: " short2 " },
{ type: "same", line: "indented line " },
{ type: "same", line: "final line " },
]
: [
{ type: "old", line: " short" },
{ type: "new", line: "short" },
{ type: "old", line: " middle" },
{ type: "new", line: "middle" },
{ type: "same", line: " a long enough line" },
{ type: "same", line: " short2" },
{ type: "same", line: "indented line" },
{ type: "same", line: "final line" },
]
expect(streamDiffs).toEqual(expected)
},
)
test("preserves original lines for minor reindentation in simple block", async () => {
const oldLines = ["if (checkValueOf(x)) {", " doSomethingWith(x);", "}"]
const newLines = ["if (checkValueOf(x)) {", " doSomethingWith(x);", "}"]
const { streamDiffs } = await collectDiffs(oldLines, newLines)
expect(streamDiffs).toEqual([
{ type: "same", line: "if (checkValueOf(x)) {" },
{ type: "same", line: " doSomethingWith(x);" },
{ type: "same", line: "}" },
])
})
test("uses new lines for nested reindentation changes", async () => {
const oldLines = ["if (checkValueOf(x)) {", " doSomethingWith(x);", "}"]
const newLines = [
"if (checkValueOf(x)) {",
" if (reallyCheckValueOf(x)) {",
" doSomethingElseWith(x);",
" }",
"}",
]
const { streamDiffs } = await collectDiffs(oldLines, newLines)
expect(streamDiffs).toEqual([
{ type: "same", line: "if (checkValueOf(x)) {" },
{ type: "new", line: " if (reallyCheckValueOf(x)) {" },
{ type: "old", line: " doSomethingWith(x);" },
{ type: "new", line: " doSomethingElseWith(x);" },
{ type: "old", line: "}" },
{ type: "new", line: " }" },
{ type: "new", line: "}" },
])
})
test("FastAPI example", async () => {
await expectDiff("fastapi.py")
})
test("FastAPI comments", async () => {
await expectDiff("add-comments.py")
})
test("Mock LLM example", async () => {
await expectDiff("mock-llm.ts")
})
})
@@ -1,76 +0,0 @@
import { DiffLine, DiffType } from "../index.js"
import { LineStream, matchLine } from "./util.js"
/**
* https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/
* Invariants:
* - new + same = newLines.length
* - old + same = oldLinesCopy.length
* ^ (above two guarantee that all lines get represented)
* - Lines are always output in order, at least among old and new separately
* - Old lines in a hunk are always output before the new lines
*/
export async function* streamDiff(oldLines: string[], newLines: LineStream): AsyncGenerator<DiffLine> {
const oldLinesCopy = [...oldLines]
// If one indentation mistake is made, others are likely. So we are more permissive about matching
let seenIndentationMistake = false
let newLineResult = await newLines.next()
while (oldLinesCopy.length > 0 && !newLineResult.done) {
const { matchIndex, isPerfectMatch, newLine } = matchLine(newLineResult.value, oldLinesCopy, seenIndentationMistake)
if (!seenIndentationMistake && newLineResult.value !== newLine) {
seenIndentationMistake = true
}
let type: DiffType
const isNewLine = matchIndex === -1
if (isNewLine) {
type = "new"
} else {
// Insert all deleted lines before match
for (let i = 0; i < matchIndex; i++) {
yield { type: "old", line: oldLinesCopy.shift()! }
}
type = isPerfectMatch ? "same" : "old"
}
switch (type) {
case "new":
yield { type, line: newLine }
break
case "same":
yield { type, line: oldLinesCopy.shift()! }
break
case "old":
yield { type, line: oldLinesCopy.shift()! }
yield { type: "new", line: newLine }
break
default:
console.error(`Error streaming diff, unrecognized diff type: ${type}`)
}
newLineResult = await newLines.next()
}
// Once at the edge, only one choice
if (newLineResult.done && oldLinesCopy.length > 0) {
for (const oldLine of oldLinesCopy) {
yield { type: "old", line: oldLine }
}
}
if (!newLineResult.done && oldLinesCopy.length === 0) {
yield { type: "new", line: newLineResult.value }
for await (const newLine of newLines) {
yield { type: "new", line: newLine }
}
}
}
@@ -1,94 +1,8 @@
import { distance } from "fastest-levenshtein"
import { ChatMessage } from "../index.js"
import type { ChatMessage } from "../index.js"
import { renderChatMessage } from "../util/messageContent.js"
export type LineStream = AsyncGenerator<string>
type MatchLineResult = {
/**
* -1 if it's a new line, otherwise the index of the first match
* in the old lines.
*/
matchIndex: number
isPerfectMatch: boolean
newLine: string
}
function linesMatchPerfectly(lineA: string, lineB: string): boolean {
return lineA === lineB && lineA !== ""
}
const END_BRACKETS = ["}", "});", "})"]
function linesMatch(lineA: string, lineB: string, linesBetween = 0): boolean {
// Require a perfect (without padding) match for these lines
// Otherwise they are edit distance 1 from empty lines and other single char lines (e.g. each other)
if (["}", "*", "});", "})"].includes(lineA.trim())) {
return lineA.trim() === lineB.trim()
}
const d = distance(lineA, lineB)
return (
// Should be more unlikely for lines to fuzzy match if they are further away
(d / Math.max(lineA.length, lineB.length) <= Math.max(0, 0.48 - linesBetween * 0.06) ||
lineA.trim() === lineB.trim()) &&
lineA.trim() !== ""
)
}
/**
* Used to find a match for a new line in an array of old lines.
*
* Return the index of the first match and whether it is a perfect match
* Also return a version of the line with correct indentation if needs fixing
*/
export function matchLine(newLine: string, oldLines: string[], permissiveAboutIndentation = false): MatchLineResult {
// Only match empty lines if it's the next one:
if (newLine.trim() === "" && oldLines[0]?.trim() === "") {
return {
matchIndex: 0,
isPerfectMatch: true,
newLine: newLine.trim(),
}
}
const isEndBracket = END_BRACKETS.includes(newLine.trim())
for (let i = 0; i < oldLines.length; i++) {
// trims trailing whitespaces from the lines before comparison
//this ensures trailing spaces don't affect matching.
const oldLineTrimmed = oldLines[i].trimEnd()
const newLineTrimmed = newLine.trimEnd()
// Don't match end bracket lines if too far away
if (i > 4 && isEndBracket) {
return { matchIndex: -1, isPerfectMatch: false, newLine }
}
if (linesMatchPerfectly(newLineTrimmed, oldLineTrimmed)) {
return { matchIndex: i, isPerfectMatch: true, newLine }
}
if (linesMatch(newLineTrimmed, oldLineTrimmed, i)) {
// This is a way to fix indentation, but only for sufficiently long lines to avoid matching whitespace or short lines
if (
newLineTrimmed.trimStart() === oldLineTrimmed.trimStart() &&
(permissiveAboutIndentation || newLine.trim().length > 8)
) {
return {
matchIndex: i,
isPerfectMatch: true,
newLine: oldLines[i],
}
}
return { matchIndex: i, isPerfectMatch: false, newLine }
}
}
return { matchIndex: -1, isPerfectMatch: false, newLine }
}
/**
* Convert a stream of arbitrary chunks to a stream of lines
*/
@@ -97,7 +11,6 @@ export async function* streamLines(
log: boolean = false,
): LineStream {
const allLines = []
let buffer = ""
try {
@@ -110,11 +23,6 @@ export async function* streamLines(
yield line
allLines.push(line)
}
// if (buffer === "" && chunk.endsWith("\n")) {
// yield "";
// allLines.push("");
// }
}
if (buffer.length > 0) {
yield buffer
@@ -130,6 +38,5 @@ export async function* streamLines(
export async function* generateLines<T>(lines: T[]): AsyncGenerator<T> {
for (const line of lines) {
yield line
// await new Promise((resolve, reject) => setTimeout(() => resolve(null), 50));
}
}
@@ -1,138 +0,0 @@
/**
* Parses a single line from an SSE (Server-Sent Events) stream.
*/
function parseSseLine(line: string): { done: boolean; data: unknown } {
if (line.startsWith("data:[DONE]") || line.startsWith("data: [DONE]")) {
return { done: true, data: undefined }
}
if (line.startsWith("data:")) {
const jsonStr = line.slice(5).trim()
try {
return { done: false, data: JSON.parse(jsonStr) }
} catch {
return { done: false, data: undefined }
}
}
if (line.startsWith(": ping")) {
return { done: true, data: undefined }
}
return { done: false, data: undefined }
}
/**
* Streams a Response body as UTF-8 text chunks.
*
* Modern implementation using native ReadableStream and TextDecoderStream APIs.
* Requires Node.js 18+ or modern browsers.
*/
export async function* streamResponse(response: Response): AsyncGenerator<string> {
// Handle client-side cancellation
if (response.status === 499) {
return
}
// Check for error responses
if (response.status !== 200) {
throw new Error(await response.text())
}
if (!response.body) {
throw new Error("No response body returned.")
}
let chunks = 0
try {
// Modern API: Use TextDecoderStream to decode the response body
const textStream = response.body.pipeThrough(new TextDecoderStream("utf-8"))
const reader = textStream.getReader()
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
yield value
chunks++
}
} finally {
reader.releaseLock()
}
} catch (e) {
if (e instanceof Error) {
// Handle graceful cancellation
if (e.name.startsWith("AbortError")) {
return
}
// Handle premature close errors
if (e.message.toLowerCase().includes("premature close")) {
if (chunks === 0) {
throw new Error("Stream was closed before any data was received. Try again. (Premature Close)")
} else {
throw new Error("The response was cancelled mid-stream. Try again. (Premature Close).")
}
}
}
throw e
}
}
/**
* Streams Server-Sent Events (SSE) from a Response.
* Parses SSE format and yields parsed data objects.
*/
export async function* streamSse(response: Response): AsyncGenerator<any> {
let buffer = ""
for await (const value of streamResponse(response)) {
buffer += value
let position: number
while ((position = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, position)
buffer = buffer.slice(position + 1)
const { done, data } = parseSseLine(line)
if (done) {
break
}
if (data) {
yield data
}
}
}
// Process any remaining buffered content
if (buffer.length > 0) {
const { done, data } = parseSseLine(buffer)
if (!done && data) {
yield data
}
}
}
/**
* Streams newline-delimited JSON from a Response.
* Each line should be a complete JSON object.
*/
export async function* streamJSON(response: Response): AsyncGenerator<any> {
let buffer = ""
for await (const value of streamResponse(response)) {
buffer += value
let position: number
while ((position = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, position)
buffer = buffer.slice(position + 1)
if (line.trim()) {
try {
const data = JSON.parse(line)
yield data
} catch {
throw new Error(`Malformed JSON sent from server: ${line}`)
}
}
}
}
}
@@ -1,10 +0,0 @@
const DEFAULT_MAX_TOKENS = 4096
const DEFAULT_CONTEXT_LENGTH = 32_768
const DEFAULT_PRUNING_LENGTH = 128000
export enum NEXT_EDIT_MODELS {
MERCURY_CODER = "mercury-coder",
INSTINCT = "instinct",
}
export { DEFAULT_CONTEXT_LENGTH, DEFAULT_MAX_TOKENS, DEFAULT_PRUNING_LENGTH }
@@ -1,8 +1,7 @@
import { Tiktoken, encodingForModel as _encodingForModel } from "js-tiktoken"
import { ChatMessage, CompiledMessagesResult, MessageContent } from "../index.js"
import { addSpaceToAnyEmptyMessages, chatMessageIsEmpty } from "./messages.js"
import { DEFAULT_PRUNING_LENGTH } from "./constants.js"
import type { MessageContent } from "../index.js"
import { llamaTokenizer } from "./llamaTokenizer.js"
interface Encoding {
encode: Tiktoken["encode"]
decode: Tiktoken["decode"]
@@ -47,7 +46,7 @@ export function encodingForModel(modelName: string): Encoding {
return (gptEncoding ??= _encodingForModel("gpt-4"))
}
function countTokens(
export function countTokens(
content: MessageContent,
// defaults to llama2 because the tokenizer tends to produce more tokens
modelName = "llama2",
@@ -57,58 +56,21 @@ function countTokens(
return content.reduce((acc, part) => {
return acc + encoding.encode(part.text ?? "", "all", []).length
}, 0)
} else {
return encoding.encode(content ?? "", "all", []).length
}
return encoding.encode(content ?? "", "all", []).length
}
function countChatMessageTokens(modelName: string, chatMessage: ChatMessage): number {
// Doing simpler, safer version of what is here:
// https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
// every message follows <|im_start|>{role/name}\n{content}<|end|>\n
const BASE_TOKENS = 4
let tokens = BASE_TOKENS
if (chatMessage.content) {
tokens += countTokens(chatMessage.content, modelName)
}
return tokens
}
/**
* Extracts and validates the user message from the end of a message array.
*
* @param messages - Array of chat messages (will be modified by popping messages)
* @returns Array of messages that form the tool sequence
*/
function extractToolSequence(messages: ChatMessage[]): ChatMessage[] {
const lastMsg = messages.pop()
if (!lastMsg || lastMsg.role !== "user") {
throw new Error("Error parsing chat history: no user message found")
}
const toolSequence: ChatMessage[] = [lastMsg]
return toolSequence
}
function pruneLinesFromTop(prompt: string, maxTokens: number, modelName: string): string {
export function pruneLinesFromTop(prompt: string, maxTokens: number, modelName: string): string {
const lines = prompt.split("\n")
// Preprocess tokens for all lines and cache them.
const lineTokens = lines.map((line) => countTokens(line, modelName))
let totalTokens = lineTokens.reduce((sum, tokens) => sum + tokens, 0)
let start = 0
const currentLines = lines.length
// Calculate initial token count including newlines
totalTokens += Math.max(0, currentLines - 1) // Add tokens for joining newlines
totalTokens += Math.max(0, currentLines - 1)
// Using indexes instead of array modifications.
// Remove lines from the top until the token count is within the limit.
while (totalTokens > maxTokens && start < currentLines) {
totalTokens -= lineTokens[start]
// Decrement token count for the removed line and its preceding/joining newline (if not the last line)
totalTokens -= lineTokens[start] ?? 0
if (currentLines - start > 1) {
totalTokens--
}
@@ -118,21 +80,17 @@ function pruneLinesFromTop(prompt: string, maxTokens: number, modelName: string)
return lines.slice(start).join("\n")
}
function pruneLinesFromBottom(prompt: string, maxTokens: number, modelName: string): string {
export function pruneLinesFromBottom(prompt: string, maxTokens: number, modelName: string): string {
const lines = prompt.split("\n")
const lineTokens = lines.map((line) => countTokens(line, modelName))
let totalTokens = lineTokens.reduce((sum, tokens) => sum + tokens, 0)
let end = lines.length
// Calculate initial token count including newlines
totalTokens += Math.max(0, end - 1) // Add tokens for joining newlines
totalTokens += Math.max(0, end - 1)
// Reverse traversal to avoid array modification
// Remove lines from the bottom until the token count is within the limit.
while (totalTokens > maxTokens && end > 0) {
end--
totalTokens -= lineTokens[end]
// Decrement token count for the removed line and its following/joining newline (if not the first line)
totalTokens -= lineTokens[end] ?? 0
if (end > 0) {
totalTokens--
}
@@ -141,180 +99,11 @@ function pruneLinesFromBottom(prompt: string, maxTokens: number, modelName: stri
return lines.slice(0, end).join("\n")
}
function pruneStringFromBottom(modelName: string, maxTokens: number, prompt: string): string {
export function pruneStringFromBottom(modelName: string, maxTokens: number, prompt: string): string {
const encoding = encodingForModel(modelName)
const tokens = encoding.encode(prompt, "all", [])
if (tokens.length <= maxTokens) {
return prompt
}
return encoding.decode(tokens.slice(0, maxTokens))
}
function pruneStringFromTop(modelName: string, maxTokens: number, prompt: string): string {
const encoding = encodingForModel(modelName)
const tokens = encoding.encode(prompt, "all", [])
if (tokens.length <= maxTokens) {
return prompt
}
return encoding.decode(tokens.slice(tokens.length - maxTokens))
}
const MAX_TOKEN_SAFETY_BUFFER = 1000
const TOKEN_SAFETY_PROPORTION = 0.02
export function getTokenCountingBufferSafety(contextLength: number) {
return Math.min(MAX_TOKEN_SAFETY_BUFFER, contextLength * TOKEN_SAFETY_PROPORTION)
}
const MIN_RESPONSE_TOKENS = 1000
function pruneRawPromptFromTop(
modelName: string,
contextLength: number,
prompt: string,
tokensForCompletion: number,
): string {
const maxTokens = contextLength - tokensForCompletion - getTokenCountingBufferSafety(contextLength)
return pruneStringFromTop(modelName, maxTokens, prompt)
}
/**
* Reconciles chat messages with available context length by intelligently pruning older messages
* while preserving critical conversation elements.
*
* Core Guidelines:
* - Always preserve the last user/tool message sequence (including any associated assistant message with tool calls)
* - Always preserve the system message and tools
* - Never allow orphaned tool responses without their corresponding tool calls
* - Remove older messages first when pruning is necessary
* - Maintain conversation coherence by flattening adjacent similar messages
*
* Process:
* 1. Handle image content conversion for models that don't support images
* 2. Extract and preserve system message
* 3. Filter out empty messages and trailing non-user/tool messages
* 4. Extract the complete tool sequence from the end (user message or assistant + tool responses)
* 5. Calculate token requirements for non-negotiable elements (system, tools, last sequence)
* 6. Prune older messages until within available token budget
* 7. Reassemble with proper ordering and flatten adjacent similar messages
*
* @param params - Configuration object containing:
* - modelName: LLM model name for token counting
* - msgs: Array of chat messages to process
* - contextLength: Maximum context length supported by the model
* - maxTokens: Maximum tokens to reserve for the response
* @returns Processed array of chat messages that fit within context constraints
* @throws Error if non-negotiable elements exceed available context
*/
function compileChatMessages({
modelName,
msgs,
knownContextLength,
maxTokens,
}: {
modelName: string
msgs: ChatMessage[]
knownContextLength: number | undefined
maxTokens: number
}): CompiledMessagesResult {
let didPrune = false
let msgsCopy: ChatMessage[] = msgs.map((m) => ({ ...m }))
// Extract system message
const systemMsg = msgsCopy.find((msg) => msg.role === "system")
msgsCopy = msgsCopy.filter((msg) => msg.role !== "system")
// Remove any empty messages or non-user/tool trailing messages
msgsCopy = msgsCopy.filter((msg) => !chatMessageIsEmpty(msg))
msgsCopy = addSpaceToAnyEmptyMessages(msgsCopy)
// Extract the tool sequence from the end of the message array
const toolSequence = extractToolSequence(msgsCopy)
// Count tokens for all messages in the tool sequence
let lastMessagesTokens = 0
for (const msg of toolSequence) {
lastMessagesTokens += countChatMessageTokens(modelName, msg)
}
// System message
let systemMsgTokens = 0
if (systemMsg) {
systemMsgTokens = countChatMessageTokens(modelName, systemMsg)
}
const contextLength = knownContextLength ?? DEFAULT_PRUNING_LENGTH
const countingSafetyBuffer = getTokenCountingBufferSafety(contextLength)
const minOutputTokens = Math.min(MIN_RESPONSE_TOKENS, maxTokens)
let inputTokensAvailable = contextLength
// Leave space for output/safety
inputTokensAvailable -= countingSafetyBuffer
inputTokensAvailable -= minOutputTokens
// Non-negotiable messages
inputTokensAvailable -= systemMsgTokens
inputTokensAvailable -= lastMessagesTokens
// Make sure there's enough context for the non-excludable items
if (knownContextLength !== undefined && inputTokensAvailable < 0) {
throw new Error(
`Not enough context available to include the system message, last user message, and tools.
There must be at least ${minOutputTokens} tokens remaining for output.
Request had the following token counts:
- contextLength: ${knownContextLength}
- counting safety buffer: ${countingSafetyBuffer}
- system message: ~${systemMsgTokens}
- max output tokens: ${maxTokens}`,
)
}
// Now remove messages till we're under the limit
let currentTotal = 0
const historyWithTokens = msgsCopy.map((message) => {
const tokens = countChatMessageTokens(modelName, message)
currentTotal += tokens
return {
...message,
tokens,
}
})
while (historyWithTokens.length > 0 && currentTotal > inputTokensAvailable) {
const message = historyWithTokens.shift()!
currentTotal -= message.tokens
didPrune = true
}
// Now reassemble
const reassembled: ChatMessage[] = []
if (systemMsg) {
reassembled.push(systemMsg)
}
reassembled.push(...historyWithTokens.map(({ tokens: _tokens, ...rest }) => rest))
reassembled.push(...toolSequence)
const inputTokens = currentTotal + systemMsgTokens + lastMessagesTokens
const availableTokens = contextLength - countingSafetyBuffer - minOutputTokens
const contextPercentage = inputTokens / availableTokens
return {
compiledChatMessages: reassembled,
didPrune,
contextPercentage,
}
}
export {
compileChatMessages,
countTokens,
pruneLinesFromBottom,
pruneLinesFromTop,
pruneRawPromptFromTop,
pruneStringFromBottom,
}
@@ -1,664 +0,0 @@
import { findLlmInfo } from "./model-info"
import { ChatCompletionCreateParams } from "openai/resources/index"
import {
ChatMessage,
Chunk,
CompletionOptions,
ILLM,
LLMFullCompletionOptions,
LLMOptions,
MessageOption,
ModelCapability,
PromptLog,
TabAutocompleteOptions,
Usage,
} from "../index.js"
import type { CacheBehavior, ILLMInteractionLog, ILLMLogger } from "../index.js"
import { mergeJson } from "../util/merge.js"
import { renderChatMessage } from "../util/messageContent.js"
import { TokensBatchingService } from "../util/TokensBatchingService.js"
import { DEFAULT_CONTEXT_LENGTH, DEFAULT_MAX_TOKENS } from "./constants.js"
import { compileChatMessages, countTokens, pruneRawPromptFromTop } from "./countTokens.js"
import {
fromChatCompletionChunk,
fromChatResponse,
LlmApiRequestType,
toChatBody,
toCompleteBody,
toFimBody,
} from "./openaiTypeConverters.js"
type InteractionStatus = "in_progress" | "success" | "error" | "cancelled"
export abstract class BaseLLM implements ILLM {
static providerName: string
get providerName(): string {
return (this.constructor as typeof BaseLLM).providerName
}
/**
* This exists because for the continue-proxy, sometimes we want to get the value of the underlying provider that is used on the server
* For example, the underlying provider should always be sent with dev data
*/
get underlyingProviderName(): string {
return this.providerName
}
autocompleteOptions?: Partial<TabAutocompleteOptions>
supportsFim(): boolean {
return false
}
supportsCompletions(): boolean {
if (["openai", "azure"].includes(this.providerName)) {
if (
this.apiBase?.includes("api.groq.com") ||
this.apiBase?.includes("api.mistral.ai") ||
this.apiBase?.includes(":1337") ||
this.apiBase?.includes("integrate.api.nvidia.com") ||
this._llmOptions.useLegacyCompletionsEndpoint?.valueOf() === false
) {
// Jan + Groq + Mistral don't support completions : (
// Seems to be going out of style...
return false
}
}
if (["groq", "mistral", "deepseek"].includes(this.providerName)) {
return false
}
return true
}
uniqueId: string
model: string
title?: string
_contextLength: number | undefined
maxStopWords?: number | undefined
completionOptions: CompletionOptions
templateMessages?: (messages: ChatMessage[]) => string
logger?: ILLMLogger
llmRequestHook?: (model: string, prompt: string) => any
apiKey?: string
apiBase?: string
cacheBehavior?: CacheBehavior
capabilities?: ModelCapability
lastRequestId: string | undefined
private _llmOptions: LLMOptions
protected openaiAdapter?: any
constructor(_options: LLMOptions) {
this._llmOptions = _options
this.lastRequestId = undefined
// Set default options
const options = {
title: (this.constructor as typeof BaseLLM).providerName,
..._options,
}
this.model = options.model
// Use ../llm-info package to autodetect certain parameters
const modelSearchString =
this.providerName === "continue-proxy" ? this.model?.split("/").pop() || this.model : this.model
const llmInfo = findLlmInfo(modelSearchString, this.underlyingProviderName)
this.title = options.title
this.uniqueId = options.uniqueId ?? "None"
this._contextLength = options.contextLength ?? llmInfo?.contextLength
this.maxStopWords = options.maxStopWords ?? this.maxStopWords
this.completionOptions = {
...options.completionOptions,
model: options.model || "gpt-4",
maxTokens:
options.completionOptions?.maxTokens ??
(llmInfo?.maxCompletionTokens
? Math.min(
llmInfo.maxCompletionTokens,
// Even if the model has a large maxTokens, we don't want to use that every time,
// because it takes away from the context length
this.contextLength / 4,
)
: DEFAULT_MAX_TOKENS),
}
this.apiKey = options.apiKey
this.apiBase = options.apiBase
if (this.apiBase && !this.apiBase.endsWith("/")) {
this.apiBase = `${this.apiBase}/`
}
this.capabilities = options.capabilities
this.autocompleteOptions = options.autocompleteOptions
}
get contextLength() {
return this._contextLength ?? DEFAULT_CONTEXT_LENGTH
}
private _templatePromptLikeMessages(prompt: string): string {
if (!this.templateMessages) {
return prompt
}
// NOTE system message no longer supported here
const msgs: ChatMessage[] = [{ role: "user", content: prompt }]
return this.templateMessages(msgs)
}
private _logEnd(
model: string,
prompt: string,
completion: string,
interaction: ILLMInteractionLog | undefined,
usage: Usage | undefined,
error?: any,
): InteractionStatus {
const promptTokens = this.countTokens(prompt)
const generatedTokens = this.countTokens(completion)
TokensBatchingService.getInstance().addTokens(model, this.providerName, promptTokens, generatedTokens)
console.log("tokensGenerated", {
model: model,
provider: this.underlyingProviderName,
promptTokens: promptTokens,
generatedTokens: generatedTokens,
})
if (typeof error === "undefined") {
interaction?.logItem({
kind: "success",
promptTokens,
generatedTokens,
usage,
})
return "success"
} else {
if (error === "cancel" || error?.name?.includes("AbortError")) {
interaction?.logItem({
kind: "cancel",
promptTokens,
generatedTokens,
usage,
})
return "cancelled"
} else {
console.log(error)
interaction?.logItem({
kind: "error",
name: error.name,
message: error.message,
promptTokens,
generatedTokens,
usage,
})
return "error"
}
}
}
private parseCompletionOptions(options: LLMFullCompletionOptions) {
const log = options.log ?? true
const raw = options.raw ?? false
options.log = undefined
const completionOptions: CompletionOptions = mergeJson(this.completionOptions, options)
return { completionOptions, logEnabled: log, raw }
}
private formatChatMessages(messages: ChatMessage[]): string {
const msgsCopy = messages ? messages.map((msg) => ({ ...msg })) : []
let formatted = ""
for (const msg of msgsCopy) {
formatted += this._formatChatMessage(msg)
}
return formatted
}
private _formatChatMessage(msg: ChatMessage): string {
let contentToShow = renderChatMessage(msg)
return `<${msg.role}>\n${contentToShow}\n\n`
}
protected async *_streamFim(
_prefix: string,
_suffix: string,
_signal: AbortSignal,
_options: CompletionOptions,
): AsyncGenerator<string, PromptLog> {
throw new Error("Not implemented")
}
protected useOpenAIAdapterFor: (LlmApiRequestType | "*")[] = []
private shouldUseOpenAIAdapter(requestType: LlmApiRequestType) {
return this.useOpenAIAdapterFor.includes(requestType) || this.useOpenAIAdapterFor.includes("*")
}
async *streamFim(
prefix: string,
suffix: string,
signal: AbortSignal,
options: LLMFullCompletionOptions = {},
): AsyncGenerator<string> {
this.lastRequestId = undefined
const { completionOptions, logEnabled } = this.parseCompletionOptions(options)
const interaction = logEnabled ? this.logger?.createInteractionLog() : undefined
let status: InteractionStatus = "in_progress"
const fimLog = `Prefix: ${prefix}\nSuffix: ${suffix}`
if (logEnabled) {
interaction?.logItem({
kind: "startFim",
prefix,
suffix,
options: completionOptions,
provider: this.providerName,
})
if (this.llmRequestHook) {
this.llmRequestHook(completionOptions.model, fimLog)
}
}
let completion = ""
try {
if (this.shouldUseOpenAIAdapter("streamFim") && this.openaiAdapter) {
const stream = this.openaiAdapter.fimStream(toFimBody(prefix, suffix, completionOptions), signal)
for await (const chunk of stream) {
if (!this.lastRequestId && typeof (chunk as any).id === "string") {
this.lastRequestId = (chunk as any).id
}
const result = fromChatCompletionChunk(chunk)
if (result) {
const content = renderChatMessage(result)
const formattedContent = this._formatChatMessage(result)
interaction?.logItem({
kind: "chunk",
chunk: formattedContent,
})
completion += formattedContent
yield content
}
}
} else {
for await (const chunk of this._streamFim(prefix, suffix, signal, completionOptions)) {
interaction?.logItem({
kind: "chunk",
chunk,
})
completion += chunk
yield chunk
}
}
status = this._logEnd(completionOptions.model, fimLog, completion, interaction, undefined)
} catch (e) {
console.error(e as Error, {
context: "llm_stream_fim",
model: completionOptions.model,
provider: this.providerName,
useOpenAIAdapter: this.shouldUseOpenAIAdapter("streamFim"),
})
status = this._logEnd(completionOptions.model, fimLog, completion, interaction, undefined, e)
throw e
} finally {
if (status === "in_progress") {
this._logEnd(completionOptions.model, fimLog, completion, interaction, undefined, "cancel")
}
}
return {
prompt: fimLog,
completion,
completionOptions,
}
}
async *streamComplete(_prompt: string, signal: AbortSignal, options: LLMFullCompletionOptions = {}) {
this.lastRequestId = undefined
const { completionOptions, logEnabled, raw } = this.parseCompletionOptions(options)
const interaction = logEnabled ? this.logger?.createInteractionLog() : undefined
let status: InteractionStatus = "in_progress"
let prompt = pruneRawPromptFromTop(
completionOptions.model,
this.contextLength,
_prompt,
completionOptions.maxTokens ?? DEFAULT_MAX_TOKENS,
)
if (!raw) {
prompt = this._templatePromptLikeMessages(prompt)
}
if (logEnabled) {
interaction?.logItem({
kind: "startComplete",
prompt,
options: completionOptions,
provider: this.providerName,
})
if (this.llmRequestHook) {
this.llmRequestHook(completionOptions.model, prompt)
}
}
let completion = ""
try {
if (this.shouldUseOpenAIAdapter("streamComplete") && this.openaiAdapter) {
if (completionOptions.stream === false) {
// Stream false
const response = await this.openaiAdapter.completionNonStream(
{ ...toCompleteBody(prompt, completionOptions), stream: false },
signal,
)
this.lastRequestId = response.id ?? this.lastRequestId
completion = response.choices[0]?.text ?? ""
yield completion
} else {
// Stream true
for await (const chunk of this.openaiAdapter.completionStream(
{
...toCompleteBody(prompt, completionOptions),
stream: true,
},
signal,
)) {
if (!this.lastRequestId && typeof (chunk as any).id === "string") {
this.lastRequestId = (chunk as any).id
}
const content = chunk.choices[0]?.text ?? ""
completion += content
interaction?.logItem({
kind: "chunk",
chunk: content,
})
yield content
}
}
} else {
for await (const chunk of this._streamComplete(prompt, signal, completionOptions)) {
completion += chunk
interaction?.logItem({
kind: "chunk",
chunk,
})
yield chunk
}
}
status = this._logEnd(completionOptions.model, prompt, completion, interaction, undefined)
} catch (e) {
console.error(e as Error, {
context: "llm_stream_complete",
model: completionOptions.model,
provider: this.providerName,
useOpenAIAdapter: this.shouldUseOpenAIAdapter("streamComplete"),
streamEnabled: completionOptions.stream !== false,
})
status = this._logEnd(completionOptions.model, prompt, completion, interaction, undefined, e)
throw e
} finally {
if (status === "in_progress") {
this._logEnd(completionOptions.model, prompt, completion, interaction, undefined, "cancel")
}
}
return {
modelTitle: this.title ?? completionOptions.model,
modelProvider: this.underlyingProviderName,
prompt,
completion,
completionOptions,
}
}
async chat(messages: ChatMessage[], signal: AbortSignal, options: LLMFullCompletionOptions = {}) {
let completion = ""
for await (const message of this.streamChat(messages, signal, options)) {
completion += renderChatMessage(message)
}
return { role: "assistant" as const, content: completion }
}
compileChatMessages(message: ChatMessage[], options: LLMFullCompletionOptions) {
let { completionOptions } = this.parseCompletionOptions(options)
completionOptions = this._modifyCompletionOptions(completionOptions)
return compileChatMessages({
modelName: completionOptions.model,
msgs: message,
knownContextLength: this._contextLength,
maxTokens: completionOptions.maxTokens ?? DEFAULT_MAX_TOKENS,
})
}
protected modifyChatBody(body: ChatCompletionCreateParams): ChatCompletionCreateParams {
return body
}
private _modifyCompletionOptions(completionOptions: CompletionOptions): CompletionOptions {
// As of 01/14/25 streaming is currently not available with o1
// See these threads:
// - https://github.com/continuedev/continue/issues/3698
// - https://community.openai.com/t/streaming-support-for-o1-o1-2024-12-17-resulting-in-400-unsupported-value/1085043
if (completionOptions.model === "o1") {
completionOptions.stream = false
}
return completionOptions
}
async *streamChat(
_messages: ChatMessage[],
signal: AbortSignal,
options: LLMFullCompletionOptions = {},
messageOptions?: MessageOption,
): AsyncGenerator<ChatMessage, PromptLog> {
this.lastRequestId = undefined
let { completionOptions } = this.parseCompletionOptions(options)
const { logEnabled } = this.parseCompletionOptions(options)
const interaction = logEnabled ? this.logger?.createInteractionLog() : undefined
let status: InteractionStatus = "in_progress"
completionOptions = this._modifyCompletionOptions(completionOptions)
let messages = _messages
// If not precompiled, compile the chat messages
if (!messageOptions?.precompiled) {
const { compiledChatMessages } = compileChatMessages({
modelName: completionOptions.model,
msgs: _messages,
knownContextLength: this._contextLength,
maxTokens: completionOptions.maxTokens ?? DEFAULT_MAX_TOKENS,
})
messages = compiledChatMessages
}
const prompt = this.templateMessages ? this.templateMessages(messages) : this.formatChatMessages(messages)
if (logEnabled) {
interaction?.logItem({
kind: "startChat",
messages,
options: completionOptions,
provider: this.providerName,
})
if (this.llmRequestHook) {
this.llmRequestHook(completionOptions.model, prompt)
}
}
let completion = ""
let usage: Usage | undefined = undefined
try {
if (this.templateMessages) {
for await (const chunk of this._streamComplete(prompt, signal, completionOptions)) {
completion += chunk
interaction?.logItem({
kind: "chunk",
chunk: chunk,
})
yield { role: "assistant", content: chunk }
}
} else {
if (this.shouldUseOpenAIAdapter("streamChat") && this.openaiAdapter) {
let body = toChatBody(messages, completionOptions)
body = this.modifyChatBody(body)
if (completionOptions.stream === false) {
// Stream false
const response = await this.openaiAdapter.chatCompletionNonStream({ ...body, stream: false }, signal)
this.lastRequestId = response.id ?? this.lastRequestId
const msg = fromChatResponse(response)
yield msg
completion = this._formatChatMessage(msg)
interaction?.logItem({
kind: "message",
message: msg,
})
} else {
// Stream true
const stream = this.openaiAdapter.chatCompletionStream(
{
...body,
stream: true,
},
signal,
)
for await (const chunk of stream) {
if (!this.lastRequestId && typeof (chunk as any).id === "string") {
this.lastRequestId = (chunk as any).id
}
const result = fromChatCompletionChunk(chunk)
if (result) {
completion += this._formatChatMessage(result)
interaction?.logItem({
kind: "message",
message: result,
})
yield result
}
}
}
} else {
for await (const chunk of this._streamChat(messages, signal, completionOptions)) {
if (chunk.role === "assistant") {
completion += this._formatChatMessage(chunk)
}
interaction?.logItem({
kind: "message",
message: chunk,
})
if (chunk.role === "assistant" && chunk.usage) {
usage = chunk.usage
}
yield chunk
}
}
}
status = this._logEnd(completionOptions.model, prompt, completion, interaction, usage)
} catch (e) {
console.error(e as Error, {
context: "llm_stream_chat",
model: completionOptions.model,
provider: this.providerName,
useOpenAIAdapter: this.shouldUseOpenAIAdapter("streamChat"),
streamEnabled: completionOptions.stream !== false,
templateMessages: !!this.templateMessages,
})
status = this._logEnd(completionOptions.model, prompt, completion, interaction, usage, e)
throw e
} finally {
if (status === "in_progress") {
this._logEnd(completionOptions.model, prompt, completion, interaction, usage, "cancel")
}
}
return {
modelTitle: this.title ?? completionOptions.model,
modelProvider: this.underlyingProviderName,
prompt,
completion,
}
}
async rerank(query: string, chunks: Chunk[]): Promise<number[]> {
if (this.shouldUseOpenAIAdapter("rerank") && this.openaiAdapter) {
const results = await this.openaiAdapter.rerank({
model: this.model,
query,
documents: chunks.map((chunk) => chunk.content),
})
// Standard OpenAI format
if (results.data && Array.isArray(results.data)) {
return results.data
.sort((a: { index: number }, b: { index: number }) => a.index - b.index)
.map((result: { relevance_score: number }) => result.relevance_score)
}
throw new Error(
`Unexpected rerank response format from ${this.providerName}. ` +
`Expected 'data' array but got: ${JSON.stringify(Object.keys(results))}`,
)
}
throw new Error(`Reranking is not supported for provider type ${this.providerName}`)
}
protected async *_streamComplete(
_prompt: string,
_signal: AbortSignal,
_options: CompletionOptions,
): AsyncGenerator<string> {
throw new Error("Not implemented")
}
protected async *_streamChat(
messages: ChatMessage[],
signal: AbortSignal,
options: CompletionOptions,
): AsyncGenerator<ChatMessage> {
if (!this.templateMessages) {
throw new Error("You must either implement templateMessages or _streamChat")
}
for await (const chunk of this._streamComplete(this.templateMessages(messages), signal, options)) {
yield { role: "assistant", content: chunk }
}
}
protected async _complete(prompt: string, signal: AbortSignal, options: CompletionOptions) {
let completion = ""
for await (const chunk of this._streamComplete(prompt, signal, options)) {
completion += chunk
}
return completion
}
countTokens(text: string): number {
return countTokens(text, this.model)
}
}
@@ -1,66 +0,0 @@
import { ChatMessage, CompletionOptions, LLMOptions } from "../../index.js"
import { BaseLLM } from "../index.js"
type MockMessage = ChatMessage | "REPEAT_LAST_MSG" | "REPEAT_SYSTEM_MSG" | "ERROR"
class MockLLM extends BaseLLM {
public completion: string = "Test Completion"
public chatStreams: MockMessage[][] | undefined
static override providerName = "mock"
constructor(options: LLMOptions) {
super(options)
this.templateMessages = undefined
this.chatStreams = options.chatStreams
}
protected override async *_streamComplete(
_prompt: string,
_signal: AbortSignal,
_options: CompletionOptions,
): AsyncGenerator<string> {
yield this.completion
}
protected override async *_streamChat(
messages: ChatMessage[],
_signal: AbortSignal,
_options: CompletionOptions,
): AsyncGenerator<ChatMessage> {
if (this.chatStreams) {
const chatStream = this.chatStreams?.[messages.filter((m) => m.role === "user").length - 1]
if (chatStream) {
for (const message of chatStream) {
switch (message) {
case "REPEAT_LAST_MSG":
yield {
role: "assistant",
content: messages[messages.length - 1].content,
}
break
case "REPEAT_SYSTEM_MSG":
yield {
role: "assistant",
content: messages.find((m) => m.role === "system")?.content || "",
}
break
case "ERROR":
throw new Error("Intentional error")
default:
yield message
}
}
}
return
}
for (const char of this.completion) {
yield {
role: "assistant",
content: char,
}
}
}
}
export { MockLLM }
@@ -1,31 +0,0 @@
import { ChatMessage } from ".."
function messageIsEmpty(message: ChatMessage): boolean {
if (typeof message.content === "string") {
return message.content.trim() === ""
}
if (Array.isArray(message.content)) {
return message.content.every((item) => item.type === "text" && item.text?.trim() === "")
}
return false
}
// some providers don't support empty messages
export function addSpaceToAnyEmptyMessages(messages: ChatMessage[]): ChatMessage[] {
return messages.map((message) => {
if (messageIsEmpty(message)) {
message.content = " "
}
return message
})
}
export function chatMessageIsEmpty(message: ChatMessage): boolean {
switch (message.role) {
case "system":
case "user":
return typeof message.content === "string" && message.content.trim() === ""
case "assistant":
return typeof message.content === "string" && message.content.trim() === ""
}
}
@@ -1,22 +0,0 @@
import { OpenAi } from "./providers/openai.js"
import { LlmInfoWithProvider, ModelProvider } from "./types.js"
const allModelProviders: ModelProvider[] = [OpenAi]
const allLlms: LlmInfoWithProvider[] = allModelProviders.flatMap((provider) =>
provider.models.map((model) => ({ ...model, provider: provider.id })),
)
export function findLlmInfo(model: string, preferProviderId?: string): LlmInfoWithProvider | undefined {
if (preferProviderId) {
const provider = allModelProviders.find((p) => p.id === preferProviderId)
const info = provider?.models.find((llm) => (llm.regex ? llm.regex.test(model) : llm.model === model))
if (info) {
return {
...info,
provider: preferProviderId,
}
}
}
return allLlms.find((llm) => (llm.regex ? llm.regex.test(model) : llm.model === model))
}
@@ -1,139 +0,0 @@
import { ModelProvider } from "../types.js"
export const OpenAi: ModelProvider = {
models: [
{
model: "gpt-3.5-turbo",
displayName: "GPT-3.5 Turbo",
contextLength: 16385,
maxCompletionTokens: 4096,
},
{
model: "gpt-3.5-turbo-0613",
displayName: "GPT-3.5 Turbo",
contextLength: 16385,
maxCompletionTokens: 4096,
},
{
model: "gpt-3.5-turbo-16k",
displayName: "GPT-3.5 Turbo 16K",
contextLength: 16384,
maxCompletionTokens: 4096,
},
{
model: "gpt-35-turbo-16k",
displayName: "GPT-3.5 Turbo 16K",
contextLength: 16384,
maxCompletionTokens: 4096,
},
{
model: "gpt-35-turbo-0613",
displayName: "GPT-3.5 Turbo (0613)",
contextLength: 4096,
maxCompletionTokens: 4096,
},
{
model: "gpt-35-turbo",
displayName: "GPT-3.5 Turbo",
contextLength: 4096,
maxCompletionTokens: 4096,
},
// gpt-4
{
model: "gpt-4",
displayName: "GPT-4",
contextLength: 8192,
maxCompletionTokens: 8192,
},
{
model: "gpt-4-32k",
displayName: "GPT-4 32K",
contextLength: 32000,
maxCompletionTokens: 8192,
},
{
model: "gpt-4-turbo-preview",
displayName: "GPT-4 Turbo Preview",
contextLength: 128000,
maxCompletionTokens: 4096,
},
{
model: "gpt-4-vision",
displayName: "GPT-4 Vision",
contextLength: 128000,
maxCompletionTokens: 4096,
},
{
model: "gpt-4-0125-preview",
displayName: "GPT-4 (0125 Preview)",
contextLength: 128000,
maxCompletionTokens: 4096,
},
{
model: "gpt-4-1106-preview",
displayName: "GPT-4 (1106 Preview)",
contextLength: 128000,
maxCompletionTokens: 4096,
},
// gpt-5
{
model: "gpt-5",
displayName: "GPT-5",
contextLength: 400000,
maxCompletionTokens: 128000,
regex: /gpt-5/,
recommendedFor: ["chat"],
},
// gpt-4o
{
model: "gpt-4o",
displayName: "GPT-4o",
contextLength: 128000,
recommendedFor: ["chat"],
},
{
model: "gpt-4o-mini",
displayName: "GPT-4o Mini",
contextLength: 128000,
recommendedFor: ["chat"],
},
// o1
{
model: "o1-preview",
displayName: "o1 Preview",
contextLength: 128000,
maxCompletionTokens: 32768,
recommendedFor: ["chat"],
},
{
model: "o1-mini",
displayName: "o1 Mini",
contextLength: 128000,
maxCompletionTokens: 65536,
recommendedFor: ["chat"],
},
{
model: "o3-mini",
displayName: "o3 Mini",
contextLength: 128000,
maxCompletionTokens: 65536,
recommendedFor: ["chat"],
},
// embed
{
model: "text-embedding-3-large",
displayName: "Text Embedding 3-Large",
recommendedFor: ["embed"],
},
{
model: "text-embedding-3-small",
displayName: "Text Embedding 3-Small",
},
{
model: "text-embedding-ada-002",
displayName: "Text Embedding Ada-002",
},
],
id: "openai",
displayName: "OpenAI",
}
@@ -1,58 +0,0 @@
type UseCase = "chat" | "autocomplete" | "rerank" | "embed"
type ParameterType = "string" | "number" | "boolean"
interface Parameter {
key: string
required: boolean
valueType: ParameterType
displayName?: string
description?: string
defaultValue?: any
}
enum ChatTemplate {
None = "none",
// TODO
}
interface LlmInfo {
model: string
// providers: string[]; // TODO: uncomment and deal with the consequences
displayName?: string
description?: string
contextLength?: number
maxCompletionTokens?: number
regex?: RegExp
chatTemplate?: ChatTemplate
/** If not set, assumes "text" only */
mediaTypes?: MediaType[]
recommendedFor?: UseCase[]
/** Any additional parameters required to configure the model */
extraParameters?: Parameter[]
}
export type LlmInfoWithProvider = LlmInfo & {
provider: string
}
enum MediaType {
Text = "text",
Image = "image",
Audio = "audio",
Video = "video",
}
export interface ModelProvider {
id: string
displayName: string
models: Omit<LlmInfo, "provider">[]
/** Any additional parameters required to configure the model
*
* (other than apiKey, apiBase, which are assumed always. And of course model and provider always required)
*/
extraParameters?: Parameter[]
}
@@ -1,108 +0,0 @@
import {
ChatCompletion,
ChatCompletionChunk,
ChatCompletionCreateParams,
ChatCompletionMessageParam,
CompletionCreateParams,
} from "openai/resources/index"
import { ChatMessage, CompletionOptions, TextMessagePart } from ".."
function toChatMessage(message: ChatMessage): ChatCompletionMessageParam {
if (message.role === "system") {
return {
role: "system",
content: message.content,
}
}
if (message.role === "assistant") {
return {
role: "assistant",
content:
typeof message.content === "string"
? message.content || " " // LM Studio (and other providers) don't accept empty content
: message.content.filter((part) => part.type === "text").map((part) => part as TextMessagePart), // can remove with newer typescript version
}
} else {
if (typeof message.content === "string") {
return {
role: "user",
content: message.content ?? " ", // LM Studio (and other providers) don't accept empty content
}
}
// Extract text from message parts
return {
role: "user",
content: message.content.map((item) => (item as TextMessagePart).text).join("") || " ",
}
}
}
export function toChatBody(messages: ChatMessage[], options: CompletionOptions): ChatCompletionCreateParams {
return {
messages: messages.map(toChatMessage),
model: options.model,
max_tokens: options.maxTokens,
temperature: options.temperature,
top_p: options.topP,
frequency_penalty: options.frequencyPenalty,
presence_penalty: options.presencePenalty,
stream: options.stream ?? true,
stop: options.stop,
}
}
export function toCompleteBody(prompt: string, options: CompletionOptions): CompletionCreateParams {
return {
prompt,
model: options.model,
max_tokens: options.maxTokens,
temperature: options.temperature,
top_p: options.topP,
frequency_penalty: options.frequencyPenalty,
presence_penalty: options.presencePenalty,
stream: options.stream ?? true,
stop: options.stop,
}
}
export function toFimBody(prefix: string, suffix: string, options: CompletionOptions): Record<string, unknown> {
return {
model: options.model,
prompt: prefix,
suffix,
max_tokens: options.maxTokens,
temperature: options.temperature,
top_p: options.topP,
frequency_penalty: options.frequencyPenalty,
presence_penalty: options.presencePenalty,
stop: options.stop,
stream: true,
} as any
}
export function fromChatResponse(response: ChatCompletion): ChatMessage {
const message = response.choices[0].message
return {
role: "assistant",
content: message.content ?? "",
}
}
export function fromChatCompletionChunk(chunk: ChatCompletionChunk): ChatMessage | undefined {
const delta = chunk.choices?.[0]?.delta
if (delta?.content) {
return {
role: "assistant",
content: delta.content,
}
}
return undefined
}
export type LlmApiRequestType = "chat" | "streamChat" | "streamComplete" | "streamFim" | "rerank"
@@ -1,253 +0,0 @@
import { ChatMessage, PromptTemplate } from "../../index.js"
import { gptEditPrompt } from "./edit/gpt.js"
const simplifiedEditPrompt = `Consider the following code:
\`\`\`{{{language}}}
{{{codeToEdit}}}
\`\`\`
Edit the code to perfectly satisfy the following user request:
{{{userInput}}}
Output nothing except for the code. No code block, no English explanation, no start/end tags.`
const simplestEditPrompt = `Here is the code before editing:
\`\`\`{{{language}}}
{{{codeToEdit}}}
\`\`\`
Here is the edit requested:
"{{{userInput}}}"
Here is the code after editing:`
const codellamaInfillEditPrompt = "{{filePrefix}}<FILL>{{fileSuffix}}"
const START_TAG = "<START EDITING HERE>"
const osModelsEditPrompt: PromptTemplate = (history, otherData) => {
// "No sufix" means either there is no suffix OR
// it's a clean break at end of function or something
// (what we're trying to avoid is just the language model trying to complete the closing brackets of a function or something)
const firstCharOfFirstLine = otherData.suffix?.split("\n")[0]?.[0]?.trim()
const isSuffix =
otherData.suffix?.trim() !== "" &&
// First character of first line is whitespace
// Otherwise we assume it's a clean break
!firstCharOfFirstLine
const suffixTag = isSuffix ? "<STOP EDITING HERE>" : ""
const suffixExplanation = isSuffix ? ' When you get to "<STOP EDITING HERE>", end your response.' : ""
// If neither prefilling nor /v1/completions are supported, we have to use a chat prompt without putting words in the model's mouth
if (otherData.supportsCompletions !== "true" && otherData.supportsPrefill !== "true") {
return gptEditPrompt(history, otherData)
}
// Use a different prompt when there's neither prefix nor suffix
if (otherData.prefix?.trim() === "" && otherData.suffix?.trim() === "") {
return [
{
role: "user",
content: `\`\`\`${otherData.language}
${otherData.codeToEdit}
${suffixTag}
\`\`\`
Please rewrite the entire code block above in order to satisfy the following request: "${otherData.userInput}". You should rewrite the entire code block without leaving placeholders, even if the code is the same as before.${suffixExplanation}`,
},
{
role: "assistant",
content: `Sure! Here's the entire rewritten code block:
\`\`\`${otherData.language}
`,
},
]
}
return [
{
role: "user",
content: `\`\`\`${otherData.language}
${otherData.prefix}${START_TAG}
${otherData.codeToEdit}
${suffixTag}
\`\`\`
Please rewrite the entire code block above, editing the portion below "${START_TAG}" in order to satisfy the following request: "${otherData.userInput}". You should rewrite the entire code block without leaving placeholders, even if the code is the same as before.${suffixExplanation}
`,
},
{
role: "assistant",
content: `Sure! Here's the entire code block, including the rewritten portion:
\`\`\`${otherData.language}
${otherData.prefix}${START_TAG}
`,
},
]
}
const mistralEditPrompt = `[INST] You are a helpful code assistant. Your task is to rewrite the following code with these instructions: "{{{userInput}}}"
\`\`\`{{{language}}}
{{{codeToEdit}}}
\`\`\`
Just rewrite the code without explanations: [/INST]
\`\`\`{{{language}}}`
const alpacaEditPrompt = `Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
### Instruction: Rewrite the code to satisfy this request: "{{{userInput}}}"
### Input:
\`\`\`{{{language}}}
{{{codeToEdit}}}
\`\`\`
### Response:
Sure! Here's the code you requested:
\`\`\`{{{language}}}
`
const phindEditPrompt = `### System Prompt
You are an expert programmer and write code on the first attempt without any errors or fillers.
### User Message:
Rewrite the code to satisfy this request: "{{{userInput}}}"
\`\`\`{{{language}}}
{{{codeToEdit}}}
\`\`\`
### Assistant:
Sure! Here's the code you requested:
\`\`\`{{{language}}}
`
const deepseekEditPrompt = `### System Prompt
You are an AI programming assistant, utilizing the DeepSeek Coder model, developed by DeepSeek Company, and your role is to assist with questions related to computer science. For politically sensitive questions, security and privacy issues, and other non-computer science questions, you will not answer.
### Instruction:
Rewrite the code to satisfy this request: "{{{userInput}}}"
\`\`\`{{{language}}}
{{{codeToEdit}}}
\`\`\`<|EOT|>
### Response:
Sure! Here's the code you requested:
\`\`\`{{{language}}}
`
const zephyrEditPrompt = `<|system|>
You are an expert programmer and write code on the first attempt without any errors or fillers.</s>
<|user|>
Rewrite the code to satisfy this request: "{{{userInput}}}"
\`\`\`{{{language}}}
{{{codeToEdit}}}
\`\`\`</s>
<|assistant|>
Sure! Here's the code you requested:
\`\`\`{{{language}}}
`
const openchatEditPrompt = `GPT4 Correct User: You are an expert programmer and personal assistant. You are asked to rewrite the following code in order to {{{userInput}}}.
\`\`\`{{{language}}}
{{{codeToEdit}}}
\`\`\`
Please only respond with code and put it inside of a markdown code block. Do not give any explanation, but your code should perfectly satisfy the user request.<|end_of_turn|>GPT4 Correct Assistant: Sure thing! Here is the rewritten code that you requested:
\`\`\`{{{language}}}
`
const xWinCoderEditPrompt = `<system>: You are an AI coding agent that helps people with programming. Write a response that appropriately completes the user's request.
<user>: Please rewrite the following code with these instructions: "{{{userInput}}}"
\`\`\`{{{language}}}
{{{codeToEdit}}}
\`\`\`
Just rewrite the code without explanations:
<AI>:
\`\`\`{{{language}}}`
const neuralChatEditPrompt = `### System:
You are an expert programmer and write code on the first attempt without any errors or fillers.
### User:
Rewrite the code to satisfy this request: "{{{userInput}}}"
\`\`\`{{{language}}}
{{{codeToEdit}}}
\`\`\`
### Assistant:
Sure! Here's the code you requested:
\`\`\`{{{language}}}
`
const codeLlama70bEditPrompt = `<s>Source: system\n\n You are an expert programmer and write code on the first attempt without any errors or fillers. <step> Source: user\n\n Rewrite the code to satisfy this request: "{{{userInput}}}"
\`\`\`{{{language}}}
{{{codeToEdit}}}
\`\`\` <step> Source: assistant\nDestination: user\n\n `
const claudeEditPrompt: PromptTemplate = (history: ChatMessage[], otherData: Record<string, string>) => [
{
role: "user",
content: `\
\`\`\`${otherData.language}
${otherData.codeToEdit}
\`\`\`
You are an expert programmer. You will rewrite the above code to do the following:
${otherData.userInput}
Output only a code block with the rewritten code:
`,
},
{
role: "assistant",
content: `Sure! Here is the rewritten code:
\`\`\`${otherData.language}`,
},
]
const llama3EditPrompt: PromptTemplate = `<|begin_of_text|><|start_header_id|>user<|end_header_id|>
\`\`\`{{{language}}}
{{{codeToEdit}}}
\`\`\`
Rewrite the above code to satisfy this request: "{{{userInput}}}"<|eot_id|><|start_header_id|>assistant<|end_header_id|>
Sure! Here's the code you requested:
\`\`\`{{{language}}}`
const gemmaEditPrompt = `<start_of_turn>user
You are an expert programmer and write code on the first attempt without any errors or fillers. Rewrite the code to satisfy this request: "{{{userInput}}}"
\`\`\`{{{language}}}
{{{codeToEdit}}}
\`\`\`<end_of_turn>
<start_of_turn>model
Sure! Here's the code you requested:
\`\`\`{{{language}}}
`
export {
alpacaEditPrompt,
claudeEditPrompt,
codeLlama70bEditPrompt,
codellamaInfillEditPrompt,
deepseekEditPrompt,
gemmaEditPrompt,
gptEditPrompt,
llama3EditPrompt,
mistralEditPrompt,
neuralChatEditPrompt,
openchatEditPrompt,
osModelsEditPrompt,
phindEditPrompt,
simplestEditPrompt,
simplifiedEditPrompt,
xWinCoderEditPrompt,
zephyrEditPrompt,
}
@@ -1,72 +0,0 @@
import { PromptTemplateFunction } from "../../.."
import { dedent } from "../../../util"
const gptInsertionEditPrompt: PromptTemplateFunction = (_, otherData) => {
return dedent`
\`\`\`${otherData.language}
${otherData.prefix}[BLANK]${otherData.codeToEdit}${otherData.suffix}
\`\`\`
Above is the file of code that the user is currently editing in. Their cursor is located at the "[BLANK]". They have requested that you fill in the "[BLANK]" with code that satisfies the following request:
"${otherData.userInput}"
Please generate this code. Your output will be only the code that should replace the "[BLANK]", without repeating any of the prefix or suffix, without any natural language explanation, and without messing up indentation. Here is the code that will replace the "[BLANK]":`
}
const gptFullFileEditPrompt: PromptTemplateFunction = (_, otherData) => {
return dedent`
\`\`\`${otherData.language}
${otherData.codeToEdit}
\`\`\`
Please rewrite the above file to address the following request:
${otherData.userInput}
You should rewrite the entire file without any natural language explanation. DO NOT surround the code in a code block and DO NOT explain yourself.`
}
export const gptEditPrompt: PromptTemplateFunction = (history, otherData) => {
if (otherData?.codeToEdit?.trim().length === 0) {
return gptInsertionEditPrompt(history, otherData)
} else if (otherData?.prefix?.trim().length === 0 && otherData?.suffix?.trim().length === 0) {
return gptFullFileEditPrompt(history, otherData)
}
const paragraphs = ["The user has requested a section of code in a file to be rewritten."]
if (otherData.prefix?.trim().length > 0) {
paragraphs.push(dedent`
This is the prefix of the file:
\`\`\`${otherData.language}
${otherData.prefix}
\`\`\``)
}
if (otherData.suffix?.trim().length > 0) {
paragraphs.push(dedent`
This is the suffix of the file:
\`\`\`${otherData.language}
${otherData.suffix}
\`\`\``)
}
paragraphs.push(dedent`
This is the code to rewrite:
\`\`\`${otherData.language}
${otherData.codeToEdit}
\`\`\`
The user's request is: "${otherData.userInput}"
DO NOT output any natural language, only output the code changes.
Here is the rewritten code:`)
return paragraphs.join("\n\n")
}
export const defaultApplyPrompt: PromptTemplateFunction = (history, otherData) => {
return `${otherData.original_code}\n\nThe following code was suggested as an edit:\n\`\`\`\n${otherData.new_code}\n\`\`\`\nPlease apply it to the previous code.`
}
@@ -1,87 +0,0 @@
import type { ILLM, TabAutocompleteOptions } from "../index.js"
import { MinimalConfigProvider } from "../autocomplete/MinimalConfig.js"
/**
* Minimal config structure for testing.
* Matches the shape of config returned by MinimalConfigProvider.loadConfig()
*/
interface MinimalTestConfig {
modelsByRole?: {
autocomplete?: ILLM[]
}
selectedModelByRole?: {
autocomplete?: ILLM
edit?: ILLM
chat?: ILLM
rerank?: ILLM
}
rules?: unknown[]
}
/**
* Options for customizing FakeConfigHandler behavior.
* All options are optional and will use sensible defaults if not provided.
*/
interface FakeConfigHandlerOptions {
/** Configuration to return from loadConfig() */
config?: Partial<MinimalTestConfig>
/** Autocomplete model to use (shorthand for setting selectedModelByRole.autocomplete) */
autocompleteModel?: ILLM
/** Whether static contextualization is enabled */
enableStaticContextualization?: boolean
/** Tab autocomplete options */
tabAutocompleteOptions?: TabAutocompleteOptions
/** Profile type for logging */
profileType?: "control-plane" | "local" | "platform"
}
export class FakeConfigHandler extends MinimalConfigProvider {
/** Track calls to onConfigUpdate for assertions */
public configUpdateCallbacks: Array<(event: { config: MinimalTestConfig; configLoadInterrupted: boolean }) => void> =
[]
constructor(options: FakeConfigHandlerOptions = {}) {
// Build config from options
const autocompleteModel = options.autocompleteModel
const config = {
tabAutocompleteOptions: options.tabAutocompleteOptions,
experimental: {
enableStaticContextualization: options.enableStaticContextualization ?? false,
},
modelsByRole: {
autocomplete: autocompleteModel ? [autocompleteModel] : [],
},
selectedModelByRole: {
autocomplete: autocompleteModel,
},
...options.config,
}
// Call parent constructor with merged config
super(config)
// Set profile if provided
if (options.profileType) {
this.currentProfile = {
profileDescription: {
profileType: options.profileType,
},
}
}
}
/**
* Register config update handler
* Overrides parent to track callbacks for test assertions
*/
override onConfigUpdate(
handler: (event: { config: MinimalTestConfig; configLoadInterrupted: boolean }) => void,
): void {
this.configUpdateCallbacks.push(handler)
}
}
@@ -1,150 +0,0 @@
import type {
DocumentSymbol,
FileStatsMap,
IDE,
IdeInfo,
Location,
Range,
RangeInFile,
SignatureHelp,
} from "../index.js"
/**
* Options for customizing FakeIDE behavior.
* All options are optional and will use sensible defaults if not provided.
*/
interface FakeIDEOptions {
/** File contents to return for readFile calls. Maps filepath -> content */
fileContents?: Map<string, string>
/** Workspace directories to return */
workspaceDirs?: string[]
/** IDE info to return */
ideInfo?: IdeInfo
/** Open files to return */
openFiles?: string[]
/** Current file to return */
currentFile?: {
isUntitled: boolean
path: string
contents: string
}
/** Clipboard content to return */
clipboardContent?: { text: string; copiedAt: string }
/** Unique ID to return */
uniqueId?: string
}
export class FakeIDE implements IDE {
private options: FakeIDEOptions
/** Track calls to onDidChangeActiveTextEditor for assertions */
public activeTextEditorCallbacks: Array<(fileUri: string) => void> = []
constructor(options: FakeIDEOptions = {}) {
this.options = options
}
async getIdeInfo(): Promise<IdeInfo> {
return (
this.options.ideInfo ?? {
ideType: "vscode",
}
)
}
async getClipboardContent(): Promise<{ text: string; copiedAt: string }> {
return (
this.options.clipboardContent ?? {
text: "",
copiedAt: new Date().toISOString(),
}
)
}
async getUniqueId(): Promise<string> {
return this.options.uniqueId ?? "fake-unique-id"
}
async getWorkspaceDirs(): Promise<string[]> {
return this.options.workspaceDirs ?? ["/workspace"]
}
async fileExists(fileUri: string): Promise<boolean> {
if (this.options.fileContents) {
return this.options.fileContents.has(fileUri)
}
return false
}
async writeFile(_path: string, _contents: string): Promise<void> {
// No-op by default, tests can override if needed
}
async saveFile(_fileUri: string): Promise<void> {
// No-op by default, tests can override if needed
}
async readFile(fileUri: string): Promise<string> {
if (this.options.fileContents) {
return this.options.fileContents.get(fileUri) ?? ""
}
return ""
}
async readRangeInFile(_fileUri: string, _range: Range): Promise<string> {
// Simplified implementation - tests can override if needed
return ""
}
async getOpenFiles(): Promise<string[]> {
return this.options.openFiles ?? []
}
async getCurrentFile(): Promise<
| undefined
| {
isUntitled: boolean
path: string
contents: string
}
> {
return this.options.currentFile
}
async getFileStats(_files: string[]): Promise<FileStatsMap> {
// Return empty map by default
return {}
}
// LSP methods - return empty arrays by default
async gotoDefinition(_location: Location): Promise<RangeInFile[]> {
return []
}
async gotoTypeDefinition(_location: Location): Promise<RangeInFile[]> {
return []
}
async getSignatureHelp(_location: Location): Promise<SignatureHelp | null> {
return null
}
async getReferences(_location: Location): Promise<RangeInFile[]> {
return []
}
async getDocumentSymbols(_textDocumentIdentifier: string): Promise<DocumentSymbol[]> {
return []
}
// Callbacks
onDidChangeActiveTextEditor(callback: (fileUri: string) => void): void {
this.activeTextEditorCallbacks.push(callback)
}
}
@@ -1,9 +1,5 @@
import { MinimalConfigProvider } from "../autocomplete/MinimalConfig"
import { FileSystemIde } from "../util/filesystem"
import { TEST_DIR } from "./testDir"
export const testIde = new FileSystemIde(TEST_DIR)
// For autocomplete/nextEdit tests, use MinimalConfigProvider
export const testMinimalConfigProvider = new MinimalConfigProvider()
@@ -1,7 +1,7 @@
import fs from "fs"
import os from "os"
import path from "path"
import { localPathOrUriToPath, localPathToUri } from "../util/pathToUri"
import { localPathToUri } from "../util/pathToUri"
// Want this outside of the git repository so we can change branches in tests
const TEST_DIR_PATH = path.join(os.tmpdir(), "testWorkspaceDir")
@@ -29,33 +29,3 @@ export function tearDownTestDir() {
})
}
}
/*
accepts array of items in 3 formats, e.g.
"index/" creates index directory
"index/index.ts" creates an empty index/index.ts
["index/index.ts", "hello"] creates index/index.ts with contents "hello"
*/
export function addToTestDir(pathsOrUris: (string | [string, string])[]) {
// Allow tests to use URIs or local paths
const paths = pathsOrUris.map((val) => {
if (Array.isArray(val)) {
return [localPathOrUriToPath(val[0]), val[1]]
} else {
return localPathOrUriToPath(val)
}
})
for (const p of paths) {
const filepath = path.join(TEST_DIR_PATH, Array.isArray(p) ? p[0] : p)
fs.mkdirSync(path.dirname(filepath), { recursive: true })
if (Array.isArray(p)) {
fs.writeFileSync(filepath, p[1])
} else if (p.endsWith("/")) {
fs.mkdirSync(filepath, { recursive: true })
} else {
fs.writeFileSync(filepath, "")
}
}
}
@@ -1,11 +0,0 @@
import fs from "fs"
import path from "path"
// Sets up the GLOBAL directory for testing - equivalent to ~/.continue
// IMPORTANT: the CONTINUE_GLOBAL_DIR environment variable is used in utils/paths for getting all local paths
export default async function () {
process.env.CONTINUE_GLOBAL_DIR = path.join(__dirname, ".continue-test")
if (fs.existsSync(process.env.CONTINUE_GLOBAL_DIR)) {
fs.rmSync(process.env.CONTINUE_GLOBAL_DIR, { recursive: true })
}
}
@@ -1,11 +0,0 @@
import { TextDecoder, TextEncoder } from "util"
import { beforeAll } from "vitest"
beforeAll(async () => {
const g: any = globalThis
// Node 20+ provides global fetch/Request/Response natively
// Set up TextEncoder/TextDecoder for tests
g.TextEncoder = TextEncoder
g.TextDecoder = TextDecoder
})
@@ -1,34 +0,0 @@
// Minimal stub for removed codeRenderer functionality
import type { DiffLine, DiffChar } from "../index.js"
export class CodeRenderer {
private static instance: CodeRenderer
static getInstance(): CodeRenderer {
if (!CodeRenderer.instance) {
CodeRenderer.instance = new CodeRenderer()
}
return CodeRenderer.instance
}
async setTheme(_theme: string): Promise<void> {
// No-op stub
}
async getDataUri(
_text: string,
_languageId: string,
_options: {
imageType: "svg"
fontSize: number
fontFamily: string
dimensions: { width: number; height: number }
lineHeight: number
},
_currLineOffsetFromTop: number,
_newDiffLines: DiffLine[],
_diffChars: DiffChar[],
): Promise<string> {
// Return empty data URI as stub
return "data:image/svg+xml;base64,"
}
}
@@ -1,82 +0,0 @@
interface TokenBatch {
model: string
provider: string
count: number
totalPromptTokens: number
totalGeneratedTokens: number
lastEventTime: number
}
export class TokensBatchingService {
private static instance: TokensBatchingService
private batches = new Map<string, TokenBatch>()
private flushTimer: NodeJS.Timeout | null = null
private readonly BATCH_SIZE_LIMIT = 25
private readonly FLUSH_INTERVAL_MS = 10 * 60 * 1000 // 10 minutes
static getInstance(): TokensBatchingService {
if (!TokensBatchingService.instance) {
TokensBatchingService.instance = new TokensBatchingService()
}
return TokensBatchingService.instance
}
private constructor() {
this.startFlushTimer()
}
addTokens(model: string, provider: string, promptTokens: number, generatedTokens: number): void {
const key = `${provider}:${model}`
if (!this.batches.has(key)) {
this.batches.set(key, {
model,
provider,
count: 0,
totalPromptTokens: 0,
totalGeneratedTokens: 0,
lastEventTime: Date.now(),
})
}
const batch = this.batches.get(key)!
batch.count++
batch.totalPromptTokens += promptTokens
batch.totalGeneratedTokens += generatedTokens
batch.lastEventTime = Date.now()
// Flush if batch is full
if (batch.count >= this.BATCH_SIZE_LIMIT) {
this.flushBatch(key, batch)
}
}
private flushBatch(key: string, batch: TokenBatch): void {
if (batch.count === 0) return
this.batches.delete(key)
}
private startFlushTimer(): void {
this.flushTimer = setInterval(() => {
this.flushAllBatches()
}, this.FLUSH_INTERVAL_MS)
// Allow the process to exit if this timer is the only thing keeping it alive
// This prevents test hangs and allows graceful shutdown
this.flushTimer.unref()
}
private flushAllBatches(): void {
for (const [key, batch] of this.batches.entries()) {
this.flushBatch(key, batch)
}
}
shutdown(): void {
if (this.flushTimer) {
clearInterval(this.flushTimer)
this.flushTimer = null
}
this.flushAllBatches()
}
}
@@ -1,51 +0,0 @@
export function dedent(strings: TemplateStringsArray, ...values: unknown[]) {
let raw = ""
for (let i = 0; i < strings.length; i++) {
raw += strings[i]
// Handle the value if it exists
if (i < values.length) {
let value = String(values[i])
// If the value contains newlines, we need to adjust the indentation
if (value.includes("\n")) {
// Find the indentation level of the last line in strings[i]
const lines = strings[i].split("\n")
const lastLine = lines[lines.length - 1]
const match = lastLine.match(/(^|\n)([^\S\n]*)$/)
const indent = match ? match[2] : ""
// Add indentation to all lines except the first line of value
let valueLines = value.split("\n")
valueLines = valueLines.map((line, index) => (index === 0 ? line : indent + line))
value = valueLines.join("\n")
}
raw += value
}
}
// Now dedent the full string
const result = raw.replace(/^\n/, "").replace(/\n\s*$/, "")
let lines = result.split("\n")
// Remove leading/trailing blank lines
while (lines.length > 0 && lines[0].trim() === "") {
lines.shift()
}
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
lines.pop()
}
// Calculate minimum indentation (excluding empty lines)
const minIndent = lines.reduce((min: number | null, line: string) => {
if (line.trim() === "") return min
const match = line.match(/^(\s*)/)
const indent = match ? match[1].length : 0
return min === null ? indent : Math.min(min, indent)
}, null)
if (minIndent !== null && minIndent > 0) {
// Remove the minimum indentation from each line
lines = lines.map((line) => line.slice(minIndent))
}
return lines.join("\n")
}
@@ -1,35 +0,0 @@
function getIconFromLevel(level: string): string {
switch (level) {
case "debug":
return "🔵"
case "info":
return "🟢"
case "warn":
return "🟡"
case "error":
return "🔴"
}
return "X"
}
export class Logger {
constructor(
private filename: string,
private includeFilename = false,
) {}
#formatMessage(level: string, message: string): string {
return `${getIconFromLevel(level)} ${this.includeFilename ? `[${this.filename}] ` : ""}${message}`
}
debug(message: string, ...args: any[]) {
console.debug(this.#formatMessage("debug", message), ...args)
}
info(message: string, ...args: any[]) {
console.info(this.#formatMessage("info", message), ...args)
}
warn(message: string, ...args: any[]) {
console.info(this.#formatMessage("warn", message), ...args)
}
error(message: string, ...args: any[]) {
console.info(this.#formatMessage("error", message), ...args)
}
}
@@ -1,58 +0,0 @@
import { ConfigMergeType } from "../index.js"
// Allow any JSON-compatible value, including complex objects
export function mergeJson<T = Record<string, unknown>>(
first: T,
second: Partial<T>,
mergeBehavior?: ConfigMergeType,
mergeKeys?: { [key: string]: (a: unknown, b: unknown) => boolean },
): T {
const copyOfFirst = JSON.parse(JSON.stringify(first))
try {
for (const key in second) {
const secondValue = second[key]
if (!(key in copyOfFirst) || mergeBehavior === "overwrite") {
// New value
copyOfFirst[key] = secondValue
continue
}
const firstValue = copyOfFirst[key]
if (Array.isArray(secondValue) && Array.isArray(firstValue)) {
// Array
if (mergeKeys?.[key]) {
// Merge keys are used to determine whether an item form the second object should override one from the first
const keptFromFirst: unknown[] = []
firstValue.forEach((item: unknown) => {
if (!secondValue.some((item2: unknown) => mergeKeys[key](item, item2))) {
keptFromFirst.push(item)
}
})
copyOfFirst[key] = [...keptFromFirst, ...secondValue]
} else {
copyOfFirst[key] = [...firstValue, ...secondValue]
}
} else if (
typeof secondValue === "object" &&
secondValue !== null &&
typeof firstValue === "object" &&
firstValue !== null
) {
// Object
copyOfFirst[key] = mergeJson(firstValue, secondValue, mergeBehavior)
} else {
// Other (boolean, number, string)
copyOfFirst[key] = secondValue
}
}
return copyOfFirst
} catch (e) {
console.error("Error merging JSON", e, copyOfFirst, second)
return {
...copyOfFirst,
...second,
}
}
}
@@ -1,4 +1,4 @@
import { TabAutocompleteOptions } from "../index.js"
import type { TabAutocompleteOptions } from "../index.js"
export const DEFAULT_AUTOCOMPLETE_OPTS: TabAutocompleteOptions = {
disable: false,
@@ -28,5 +28,3 @@ export const DEFAULT_AUTOCOMPLETE_OPTS: TabAutocompleteOptions = {
experimental_includeDiff: true,
experimental_enableStaticContextualization: false,
}
export const COUNT_COMPLETION_REJECTED_AFTER = 10_000
@@ -1,34 +0,0 @@
import * as fs from "fs"
import * as os from "os"
import * as path from "path"
const CONTINUE_GLOBAL_DIR = (() => {
const configPath = process.env.CONTINUE_GLOBAL_DIR
if (configPath) {
// Convert relative path to absolute paths based on current working directory
return path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath)
}
return path.join(os.homedir(), ".continue")
})()
function getContinueGlobalPath(): string {
// This is ~/.continue on mac/linux
const continuePath = CONTINUE_GLOBAL_DIR
if (!fs.existsSync(continuePath)) {
fs.mkdirSync(continuePath)
}
return continuePath
}
function getIndexFolderPath(): string {
const indexPath = path.join(getContinueGlobalPath(), "index")
if (!fs.existsSync(indexPath)) {
fs.mkdirSync(indexPath)
}
return indexPath
}
export function getConfigJsonPath(): string {
const p = path.join(getContinueGlobalPath(), "config.json")
return p
}
@@ -1,18 +1,3 @@
/** Converts any OS path to cleaned up URI path segment format with no leading/trailing slashes
e.g. \path\to\folder\ -> path/to/folder
\this\is\afile.ts -> this/is/afile.ts
is/already/clean -> is/already/clean
**/
function pathToUriPathSegment(path: string) {
let clean = path.replace(/[\\]/g, "/") // backslashes -> forward slashes
clean = clean.replace(/^\//, "") // remove start slash
clean = clean.replace(/\/$/, "") // remove end slash
return clean
.split("/")
.map((part) => encodeURIComponent(part))
.join("/")
}
function getCleanUriPath(uri: string) {
// Handle both URIs and plain paths
let path: string
@@ -106,15 +91,6 @@ export function getLastNUriRelativePathParts(dirUriCandidates: string[], uri: st
return getLastNPathParts(relativePathOrBasename, n)
}
export function joinPathsToUri(uri: string, ...pathSegments: string[]) {
let baseUri = uri
if (baseUri.at(-1) !== "/") {
baseUri += "/"
}
const segments = pathSegments.map((segment) => pathToUriPathSegment(segment))
return new URL(segments.join("/"), baseUri).toString()
}
export function getShortestUniqueRelativeUriPaths(
uris: string[],
dirUriCandidates: string[],
@@ -1,761 +0,0 @@
# Transplant Plan: `src/services/autocomplete/` → standalone VS Code extension
## 1. Executive Summary
The `src/services/autocomplete/` module provides two autocomplete experiences:
1. **Inline code completion (ghost text)** via a VS Code [`vscode.InlineCompletionItemProvider`](src/services/autocomplete/docs/investigation-vscode-integration.md:245).
2. **Chat textarea autocomplete** for a webview chat input (optional), driven by webview messages ([`requestChatCompletion`](src/services/autocomplete/docs/investigation-vscode-integration.md:188)).
To transplant this module into a new VS Code extension, you can copy most of the directory tree as-is, but you must rebuild a small “host shell” around it:
- A VS Code extension activation/registration layer (commands, providers, status bar, context keys).
- A **settings/state store** that persists the autocomplete settings object.
- An **LLM provider abstraction** that supports streaming **FIM** (fill-in-the-middle) and streaming **chat completions**, plus model metadata and usage/cost reporting.
- A **file ignore / access control abstraction** (to replicate `.kilocodeignore` behavior and prevent sensitive files from being used).
- A **telemetry abstraction** for the events the module emits.
- (Optional) A webview messaging bridge if you want a UI for settings or chat textarea autocomplete.
This plan defines those interfaces and the minimum VS Code extension scaffolding required, without coupling the new extension to Kilo Code’s current provider implementations.
---
## 2. Interfaces to Implement
This section defines the abstract interfaces the transplanted module expects.
> Design principle: the autocomplete module should depend on small, stable interfaces and plain types, not the host extension’s internal classes.
### 2.1 `IAutocompleteLLMProvider` (LLM Provider Interface)
**Purpose**: Provide streaming LLM completions for both strategies:
- **FIM** (prefix + suffix → streamed insertion)
- **Chat completion** (system prompt + user prompt → streamed text and/or structured chunks)
The module currently routes these via [`AutocompleteModel.generateFimResponse()`](src/services/autocomplete/AutocompleteModel.ts:109) and [`AutocompleteModel.generateResponse()`](src/services/autocomplete/AutocompleteModel.ts:153).
#### Required API (proposed)
```ts
export interface AutocompleteUsage {
cost: number
inputTokens: number
outputTokens: number
cacheWriteTokens: number
cacheReadTokens: number
}
export interface AutocompleteModelInfo {
providerId: string // stable identifier, e.g. openai, anthropic, custom
providerDisplayName?: string // for status bar/UI
modelId: string // stable model identifier
modelDisplayName?: string
supportsFim: boolean
}
export type ChatStreamChunk = { type: "text"; text: string } | { type: "usage"; usage: AutocompleteUsage }
export interface IAutocompleteLLMProvider {
/**
* Returns the currently selected model/provider metadata.
* Used for status bar + telemetry context.
*/
getModelInfo(): AutocompleteModelInfo | undefined
/**
* Whether the selected model supports FIM.
* Used to pick between FIM vs hole-filler strategy.
*/
supportsFim(): boolean
/**
* Stream a FIM completion. The generator yields raw text chunks.
* Must be abortable.
*/
streamFim(params: {
prefix: string
suffix: string
signal: AbortSignal
requestId?: string
onUsage?: (usage: AutocompleteUsage) => void
}): AsyncGenerator<string>
/**
* Stream a chat completion. The generator yields text chunks and MAY yield usage.
* Must be abortable.
*/
streamChat(params: {
systemPrompt: string
userPrompt: string
signal: AbortSignal
requestId?: string
}): AsyncGenerator<ChatStreamChunk>
}
```
#### Where it is used
- Inline completion pipeline triggers either:
- FIM flow via [`AutocompleteModel.generateFimResponse()`](src/services/autocomplete/AutocompleteModel.ts:109), or
- Chat flow via [`AutocompleteModel.generateResponse()`](src/services/autocomplete/AutocompleteModel.ts:153).
- Strategy selection checks [`AutocompleteModel.supportsFim()`](src/services/autocomplete/AutocompleteModel.ts:98).
#### Notes / constraints
- **Streaming is mandatory**: the module assumes tokens/chunks arrive incrementally.
- **Abort is mandatory**: VS Code frequently cancels inline completion requests.
- **Usage/cost reporting** is needed for:
- status bar display (session cost), and
- telemetry properties (latency/cost/tokens).
---
### 2.2 `IAutocompleteProfileResolver` (Model selection + credentials)
**Purpose**: Choose which provider/model to use for autocomplete, and validate credentials.
In the current code, [`AutocompleteModel.reload()`](src/services/autocomplete/AutocompleteModel.ts:49) scans “profiles” from a settings manager and picks one (including special handling for the `kilocode` provider).
For a new extension, keep this logic but abstract it.
#### Required API (proposed)
```ts
export interface AutocompleteProfile {
id: string
name?: string
type?: "autocomplete" | "general"
providerId: string
modelId: string
// provider-specific credential payload is opaque to autocomplete
credentials: unknown
}
export interface IAutocompleteProfileResolver {
/** Return all configured profiles the host wants autocomplete to consider. */
listProfiles(): Promise<AutocompleteProfile[]>
/** Resolve the full profile (including credentials) for a selected profile id. */
getProfile(id: string): Promise<AutocompleteProfile>
/**
* Create an LLM provider instance for the selected profile.
* The autocomplete module treats the provider as opaque beyond the interface.
*/
buildLLMProvider(profile: AutocompleteProfile): Promise<IAutocompleteLLMProvider>
}
```
#### Where it is used
- Provider/model selection and (optional) credential checks happen in [`AutocompleteModel.reload()`](src/services/autocomplete/AutocompleteModel.ts:49).
---
### 2.3 `IAutocompleteSettingsStore` (Settings/State Manager)
**Purpose**: Persist and retrieve the autocomplete settings object.
The module currently uses a global state key named `ghostServiceSettings` via `ContextProxy` ([investigation](src/services/autocomplete/docs/investigation-vscode-integration.md:201)).
#### Settings schema (minimum)
From [`autocompleteServiceSettingsSchema`](src/services/autocomplete/docs/investigation-vscode-integration.md:216), the settings object is effectively:
```ts
export interface AutocompleteServiceSettings {
enableAutoTrigger?: boolean
enableSmartInlineTaskKeybinding?: boolean
enableChatAutocomplete?: boolean
provider?: string
model?: string
snoozeUntil?: number
hasKilocodeProfileWithNoBalance?: boolean
}
```
The host should own validation (zod or equivalent). The autocomplete module assumes the object exists or is `undefined`.
#### Required API (proposed)
```ts
export interface IAutocompleteSettingsStore {
getSettings(): Promise<AutocompleteServiceSettings | undefined>
setSettings(settings: AutocompleteServiceSettings | undefined): Promise<void>
/** Optional: subscribe for settings changes coming from UI/webview. */
onDidChangeSettings?(listener: (s: AutocompleteServiceSettings | undefined) => void): { dispose(): void }
}
```
#### Where it is used
- Read settings on startup in [`AutocompleteServiceManager.load()`](src/services/autocomplete/docs/investigation-vscode-integration.md:133).
- Write enriched settings back after load (same section).
- Webview can update settings by message type `ghostServiceSettings` ([`webviewMessageHandler`](src/services/autocomplete/docs/investigation-vscode-integration.md:164)).
---
### 2.4 `IFileIgnoreController` (File Ignore Controller)
**Purpose**: Decide whether the module may read/use a file path for context.
In current code this is `RooIgnoreController` (see mock interface in [`RooIgnoreController`](src/core/ignore/__mocks__/RooIgnoreController.ts:3)). It is used for:
- Filtering/snippet inclusion (only-my-code, ignore patterns)
- Visible editor context filtering
#### Required API (proposed)
```ts
export interface IFileIgnoreController {
initialize(): Promise<void>
/** True if the file can be read/used as context. */
validateAccess(filePath: string): boolean
/** Filter a list of candidate paths to those allowed. */
filterPaths(paths: string[]): string[]
/** Optional: returns user-facing instructions explaining why access is restricted. */
getInstructions(): string | undefined
dispose(): void
}
```
#### Where it is used
- Inline completion gating checks include ignore validation ([architecture summary](src/services/autocomplete/docs/investigation-internal-architecture.md:161)).
- Visible editor context is filtered in [`VisibleCodeTracker`](src/services/autocomplete/docs/investigation-internal-architecture.md:339).
---
### 2.5 `IDE` / `VsCodeIde` (IDE Abstraction)
**Purpose**: Provide an IDE-agnostic layer used by the embedded Continue.dev fork.
The continuedev core defines an `IDE` interface in [`continuedev/core/index.d.ts`](src/services/autocomplete/continuedev/core/index.d.ts:376) and ships a VS Code implementation [`VsCodeIde`](src/services/autocomplete/continuedev/core/vscode-test-harness/src/VSCodeIde.ts:1).
#### Minimum needed methods
The module’s autocomplete pipeline uses the `IDE` abstraction for:
- Workspace discovery, reading and writing files
- Open/current file content
- LSP calls (definitions, references, symbols)
- Clipboard access
- Editor-change callback
The authoritative method list is the `IDE` interface in [`IDE`](src/services/autocomplete/continuedev/core/index.d.ts:376). For transplantation you have two viable options:
1. **Copy and keep `VsCodeIde`** as-is, and keep the `IDE` interface unchanged.
2. Replace with your own implementation, but it must still satisfy the `IDE` interface contract.
---
### 2.6 `ITelemetryClient` (Telemetry Interface)
**Purpose**: Record the module’s key product events.
Current code uses a singleton `TelemetryService` ([`AutocompleteTelemetry`](src/services/autocomplete/classic-auto-complete/AutocompleteTelemetry.ts:57)). To transplant cleanly, replace it with an injected interface.
#### Events currently captured
From [`AutocompleteTelemetry`](src/services/autocomplete/classic-auto-complete/AutocompleteTelemetry.ts:105):
- `AUTOCOMPLETE_SUGGESTION_REQUESTED`
- `AUTOCOMPLETE_SUGGESTION_FILTERED`
- `AUTOCOMPLETE_SUGGESTION_CACHE_HIT`
- `AUTOCOMPLETE_LLM_SUGGESTION_RETURNED`
- `AUTOCOMPLETE_LLM_REQUEST_COMPLETED`
- `AUTOCOMPLETE_LLM_REQUEST_FAILED`
- `AUTOCOMPLETE_ACCEPT_SUGGESTION`
- `AUTOCOMPLETE_UNIQUE_SUGGESTION_SHOWN`
Additional events mentioned in integration doc:
- `INLINE_ASSIST_AUTO_TASK`
- `GHOST_SERVICE_DISABLED`
See telemetry summary in [`investigation-vscode-integration.md`](src/services/autocomplete/docs/investigation-vscode-integration.md:357).
#### Required API (proposed)
```ts
export type TelemetryEventName = string
export interface ITelemetryClient {
captureEvent(event: TelemetryEventName, properties?: Record<string, unknown>): void
}
```
#### Where it is used
- Inline completion telemetry: [`AutocompleteTelemetry`](src/services/autocomplete/classic-auto-complete/AutocompleteTelemetry.ts:57)
- Service-level disable / code-suggestion telemetry: referenced in [`investigation-vscode-integration.md`](src/services/autocomplete/docs/investigation-vscode-integration.md:357)
---
### 2.7 `IWebviewBridge` (Optional)
**Purpose**: If your new extension has a settings UI and/or chat panel, you need a bridge for messages.
Current integration uses `ClineProvider` for posting state and receiving messages (see host dependencies list in [`investigation-vscode-integration.md`](src/services/autocomplete/docs/investigation-vscode-integration.md:367)).
Minimum message types used by autocomplete (optional but defined):
- `ghostServiceSettings` (write settings)
- `snoozeAutocomplete`
- `requestChatCompletion`
- `chatCompletionAccepted`
If you are not building a webview, you can skip this and omit `chat-autocomplete/` entirely.
---
## 3. VSCode Extension Shell
This section describes what the new extension must contribute to make the module functional.
### 3.1 `package.json`
#### 3.1.1 Activation events
Current Kilo Code activates broadly:
- `onLanguage`
- `onStartupFinished`
See [`src/package.json` snippet in investigation](src/services/autocomplete/docs/investigation-vscode-integration.md:13).
For a standalone extension you can keep these, or narrow them (e.g. only `onStartupFinished`).
#### 3.1.2 Commands
Commands declared in Kilo Code’s `package.json` (some are placeholders):
- `kilo-code.autocomplete.generateSuggestions` (registered)
- `kilo-code.autocomplete.cancelSuggestions` (declared, not registered)
- `kilo-code.autocomplete.applyCurrentSuggestions` (declared, not registered)
- `kilo-code.autocomplete.applyAllSuggestions` (declared, not registered)
- `kilo-code.autocomplete.goToNextSuggestion` (declared, not registered)
- `kilo-code.autocomplete.goToPreviousSuggestion` (declared, not registered)
See command table in [`investigation-vscode-integration.md`](src/services/autocomplete/docs/investigation-vscode-integration.md:23).
Commands registered programmatically (must be declared if you want them visible/consistent):
- `kilo-code.autocomplete.reload` ([`index.ts`](src/services/autocomplete/docs/investigation-vscode-integration.md:117))
- `kilo-code.autocomplete.codeActionQuickFix` (stub)
- `kilo-code.autocomplete.showIncompatibilityExtensionPopup`
- `kilo-code.autocomplete.disable`
- `kilocode.autocomplete.inline-completion.accepted` (acceptance callback)
- `kilo-code.jetbrains.getInlineCompletions` (JetBrains bridge)
Recommendation for the new extension:
- Keep only the commands you truly support.
- If you do not implement “suggested edits” UX, you can drop `apply*` and `goTo*` placeholders.
#### 3.1.3 Keybindings
Kilo Code binds:
- `Escape` → cancel suggestions (but depends on a context key that is never set)
- `Ctrl+L` / `Cmd+L` → generate suggestions (with Copilot conflict split)
See keybindings in [`investigation-vscode-integration.md`](src/services/autocomplete/docs/investigation-vscode-integration.md:47).
Recommendation:
- Only contribute keybindings once you implement the associated command end-to-end.
- Ensure any context keys referenced in `when` clauses are actually set.
#### 3.1.4 Code actions
If you want the Quick Fix entry point (“Suggested edits”), keep:
- `contributes.codeActions` declaration
- Register a `CodeActionsProvider` programmatically (as Kilo Code does)
See code action registration in [`investigation-vscode-integration.md`](src/services/autocomplete/docs/investigation-vscode-integration.md:72).
#### 3.1.5 Configuration contributions
Kilo Code does **not** contribute `configuration` settings; it stores everything in global state.
For a new extension you can choose either:
1. **Global state only** (closest transplant), or
2. **Real VS Code settings** (`contributes.configuration`) and have your settings store bridge to `workspace.getConfiguration`.
### 3.2 `extension.ts` (activation)
Minimum activation responsibilities:
1. Construct/inject dependencies:
- `IAutocompleteSettingsStore`
- `IAutocompleteProfileResolver` / `IAutocompleteLLMProvider`
- `IFileIgnoreController` factory
- `ITelemetryClient`
- (optional) `IWebviewBridge`
2. Initialize ignore controller and settings defaults.
3. Create the autocomplete manager and register VS Code providers.
The current integration is via `registerAutocompleteProvider(context, provider)` (see activation notes in [`investigation-vscode-integration.md`](src/services/autocomplete/docs/investigation-vscode-integration.md:98)).
Recommendation for transplant structure:
- Keep `src/services/autocomplete/index.ts` but change it to accept a dependency container instead of a `ClineProvider`.
---
## 4. External Dependencies (npm packages)
This list is derived from the external import investigation.
### 4.1 Direct dependencies (autocomplete module)
- `zod` (JetBrains bridge + continuedev adapters) ([imports list](src/services/autocomplete/docs/investigation-external-imports.md:240))
- `web-tree-sitter` (continuedev tree-sitter integration) ([imports list](src/services/autocomplete/docs/investigation-external-imports.md:252))
- `diff` (continuedev diff utilities) ([imports list](src/services/autocomplete/docs/investigation-external-imports.md:266))
- `fastest-levenshtein` (text similarity) ([imports list](src/services/autocomplete/docs/investigation-external-imports.md:273))
- `lru-cache` and `quick-lru` (caching) ([imports list](src/services/autocomplete/docs/investigation-external-imports.md:286))
- `ignore` (gitignore-like matching in continuedev) ([imports list](src/services/autocomplete/docs/investigation-external-imports.md:300))
- `js-tiktoken` (token counting) ([imports list](src/services/autocomplete/docs/investigation-external-imports.md:280))
- `uri-js` (URI parsing in `VSCodeIde`) ([imports list](src/services/autocomplete/docs/investigation-external-imports.md:313))
### 4.2 LLM SDKs (only if you keep continuedev’s provider adapters)
The continuedev fork includes many LLM adapters and imports these SDKs:
- `openai`
- `@anthropic-ai/sdk`
- `@aws-sdk/client-bedrock-runtime`
- `@aws-sdk/credential-providers`
- `google-auth-library`
- `dotenv`
See list in [`investigation-external-imports.md`](src/services/autocomplete/docs/investigation-external-imports.md:169).
If the new extension has its own LLM system, consider:
- either stripping unused continuedev adapters to reduce dependency footprint, or
- leaving them but ensuring they do not bloat the extension bundle (tree-shaking).
### 4.3 Tree-sitter WASM assets
You must ship tree-sitter WASM and query assets required by continuedev:
- `web-tree-sitter` expects parser initialization with a `.wasm` file.
- Language grammars are needed to parse various file types.
- Query files live under [`continuedev/tree-sitter/`](src/services/autocomplete/docs/investigation-internal-architecture.md:143) and should be copied.
Plan for the new extension:
- Bundle the wasm assets in your extension `dist` or `media` folder.
- Ensure runtime code can resolve them (using `ExtensionContext.extensionUri`).
---
## 5. Files to Copy As-Is
The following parts are designed to be largely self-contained (per architecture investigation):
### 5.1 Continue.dev fork (library)
Copy the entire directory:
- [`src/services/autocomplete/continuedev/`](src/services/autocomplete/continuedev/core/index.d.ts:1)
This includes context gathering, templating, postprocessing, tree-sitter queries, and utility helpers.
### 5.2 Inline completion implementation
Copy:
- `src/services/autocomplete/classic-auto-complete/` (all files)
- `src/services/autocomplete/context/` (visible code tracker)
- `src/services/autocomplete/types.ts`
### 5.3 VS Code UX helpers
Copy:
- `src/services/autocomplete/AutocompleteStatusBar.ts`
- `src/services/autocomplete/AutocompleteCodeActionProvider.ts`
### 5.4 Optional chat textarea autocomplete
Copy if you have a webview chat UI:
- `src/services/autocomplete/chat-autocomplete/`
---
## 6. Files Requiring Modification
Most modifications are to replace Kilo Code specific imports with the new interfaces.
### 6.1 `AutocompleteModel.ts`
File: [`AutocompleteModel`](src/services/autocomplete/AutocompleteModel.ts:26)
Why:
- Currently depends on `src/api` handlers and `ProviderSettingsManager`.
- Also depends on webview UI constant `PROVIDERS`.
Change plan:
- Replace `ProviderSettingsManager` usage with `IAutocompleteProfileResolver`.
- Replace `buildApiHandler` / `ApiHandler` / `FimHandler` with `IAutocompleteLLMProvider`.
- Replace `PROVIDERS` mapping with `providerDisplayName` from `AutocompleteModelInfo`.
- Keep the public surface area stable where possible:
- `supportsFim()`
- `generateFimResponse()`
- `generateResponse()`
- `getModelName()` / `getProviderDisplayName()`
### 6.2 `AutocompleteServiceManager.ts` and `index.ts`
Why:
- Current glue code expects `ContextProxy`, `ClineProvider`, and `TelemetryService`.
Change plan:
- Inject `IAutocompleteSettingsStore`, `ITelemetryClient`, ignore-controller factory.
- Replace any webview posting with an optional `IWebviewBridge`.
- Ensure `setContext` keys are correctly set (notably `kilocode.autocomplete.enableSmartInlineTaskKeybinding`, see [`investigation-vscode-integration.md`](src/services/autocomplete/docs/investigation-vscode-integration.md:145)).
### 6.3 `types.ts`
Why:
- Imports `RooIgnoreController` directly.
- Exposes `AutocompleteContextProvider` referencing `VsCodeIde` and `AutocompleteModel`.
Change plan:
- Replace `RooIgnoreController` type with `IFileIgnoreController`.
- Keep `VsCodeIde` dependency if you keep continuedev’s VS Code harness; otherwise replace with your own `IDE` implementation.
### 6.4 `VisibleCodeTracker.ts`
Why:
- Depends on `RooIgnoreController` and path utils.
Change plan:
- Replace ignore controller import with `IFileIgnoreController`.
- Replace `toRelativePath` with an equivalent helper in the new extension.
### 6.5 JetBrains bridge
File: `src/services/autocomplete/AutocompleteJetbrainsBridge.ts`.
Why:
- Uses internal Kilo Code wrapper and webview provider.
Change plan:
- If the new extension does not support JetBrains, omit it.
- If you do, implement a dedicated transport layer; keep the bridge logic but replace:
- `ClineProvider`
- `getKiloCodeWrapperProperties`
- any Kilo Code-specific types
---
## 7. i18n Setup
There are two separate i18n mechanisms involved.
### 7.1 `package.nls.json` (command titles)
Kilo Code’s `package.json` command titles reference NLS keys like:
- `autocomplete.commands.generateSuggestions`
- `autocomplete.commands.cancelSuggestions`
- …
See full list in [`investigation-vscode-integration.md`](src/services/autocomplete/docs/investigation-vscode-integration.md:269).
Transplant options:
1. Reuse the same keys and provide at least `package.nls.json` (and optionally localized variants).
2. Rename keys and update command declarations accordingly.
### 7.2 Runtime `t()` keys (status bar/tooltips/progress)
Runtime strings are accessed via `t()` from `src/i18n` (Kilo Code).
Keys used live under namespace `kilocode:autocomplete.*` (see JSON excerpt in [`investigation-vscode-integration.md`](src/services/autocomplete/docs/investigation-vscode-integration.md:287)).
Transplant plan:
- If the new extension already has i18n, add equivalent keys to its runtime translation system.
- Otherwise, simplest is:
- implement a tiny `t(key, vars?)` function that maps to a JSON bundle.
Minimum runtime keys needed for feature completeness:
- status bar labels + tooltips (enabled/disabled/snoozed + provider/model + cost)
- progress titles (analyzing/generating/processing/showing)
- incompatibility popup text (if you keep Copilot conflict logic)
---
## 8. Webview Integration (Optional)
If you want a settings UI or chat panel, you need:
### 8.1 State shape
Expose `ghostServiceSettings` (or renamed equivalent) to the webview state.
See state passing described in [`investigation-vscode-integration.md`](src/services/autocomplete/docs/investigation-vscode-integration.md:154).
### 8.2 Message types
Support these messages:
- `ghostServiceSettings` → validate + persist → trigger reload
- `snoozeAutocomplete` → snooze/unsnooze
- `requestChatCompletion` → call chat textarea autocomplete pipeline → respond `chatCompletionResult`
- `chatCompletionAccepted` → fire telemetry
The message handling mapping is documented in [`investigation-vscode-integration.md`](src/services/autocomplete/docs/investigation-vscode-integration.md:164).
---
## 9. MockTextDocument
The JetBrains bridge depends on `MockTextDocument` which is outside the autocomplete directory.
Copy:
- [`src/services/mocking/MockTextDocument.ts`](src/services/mocking/MockTextDocument.ts:1)
If you don’t transplant JetBrains support, you can skip this file.
---
## 10. Migration Checklist
Use this as the practical step-by-step transplant procedure.
### 10.1 Prepare new extension
1. Create a new VS Code extension repo (TypeScript).
2. Ensure build pipeline can bundle wasm assets.
3. Add required dependencies (Section 4).
### 10.2 Copy files
1. Copy `src/services/autocomplete/` directory into the new extension.
2. If supporting JetBrains bridge, also copy [`MockTextDocument`](src/services/mocking/MockTextDocument.ts:1).
3. Copy tree-sitter query assets under `continuedev/tree-sitter/`.
### 10.3 Implement host interfaces
1. Implement [`IAutocompleteSettingsStore`](src/services/autocomplete/docs/TRANSPLANT-PLAN.md:1) (this document) using either:
- `ExtensionContext.globalState`, or
- `workspace.getConfiguration`.
2. Implement [`IFileIgnoreController`](src/services/autocomplete/docs/TRANSPLANT-PLAN.md:1) (this document).
3. Implement [`ITelemetryClient`](src/services/autocomplete/docs/TRANSPLANT-PLAN.md:1) (this document).
4. Implement [`IAutocompleteProfileResolver`](src/services/autocomplete/docs/TRANSPLANT-PLAN.md:1) (this document).
5. Implement [`IAutocompleteLLMProvider`](src/services/autocomplete/docs/TRANSPLANT-PLAN.md:1) (this document) backed by the new extension’s LLM system.
### 10.4 Refactor autocomplete glue
1. Refactor [`AutocompleteModel`](src/services/autocomplete/AutocompleteModel.ts:26) to use the new LLM/provider resolver interfaces.
2. Refactor service initialization ([`AutocompleteServiceManager`](src/services/autocomplete/docs/investigation-vscode-integration.md:125)) to use the new settings/telemetry/ignore interfaces.
3. Refactor `index.ts` entry point to accept dependency injection rather than Kilo Code’s provider.
### 10.5 Wire VS Code extension shell
1. Implement activation in `extension.ts`:
- instantiate dependencies
- call `registerAutocompleteProvider(...)`
2. Add `package.json` contributions:
- activation events
- commands
- keybindings (optional)
- codeActions contribution (optional)
3. Ensure context keys used in `when` clauses are set via `vscode.commands.executeCommand("setContext", ...)`.
### 10.6 Validate runtime behavior
1. Inline completion:
- confirm cancellation works
- confirm debounce behavior
- confirm suggestions appear only for allowed files
2. Status bar:
- confirm provider/model shown
- confirm cost increments
3. (Optional) Webview:
- confirm settings update triggers reload
- confirm chat textarea completion roundtrip works
## 11. Clarifications
These decisions were made before implementation began and override any conflicting guidance in the sections above.
### 11.1 LLM Provider Architecture
**Decision: Option A — Via CLI backend (Kilo Gateway).**
FIM is postponed; only the **holefiller** (chat-completion-based) strategy will be used initially. The LLM provider will route completions through the Kilo Gateway backend. The only supported model for now is `mistralai/codestral-2508`.
### 11.2 Provider & Model Selection
**Decision: Hardcoded to Kilo Gateway + `mistralai/codestral-2508`.**
No profile resolver is needed in phase 1. The provider and model are fixed.
### 11.3 Feature Scope
| Feature | Decision |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| JetBrains bridge | **Exclude permanently** — delete `AutocompleteJetbrainsBridge.ts` and its tests. This will never be implemented this way. |
| Chat textarea autocomplete | **Include** |
| Code actions | **Include** |
| Continuedev LLM adapters | **Strip to minimum** — remove all adapters not needed for the Kilo Gateway / holefiller path. The module uses `AutocompleteModel`, not the continuedev adapters directly, so most can be removed. |
### 11.4 Settings Storage
**Decision: VS Code settings** via `contributes.configuration` in `package.json`.
### 11.5 Telemetry
**Decision: Console logging only.** The extension has no telemetry system yet. Keep console logs but do not send actual telemetry events. Implement a no-op `ITelemetryClient`.
### 11.6 i18n
**Decision: Wire into `@kilocode/kilo-i18n`.** Use the translations from `src/services/autocomplete/i18n/`. Keys may differ from what `kilo-i18n` uses, so mapping is required. Discard translations for locales not present in `kilo-i18n`.
### 11.7 File Ignore / Access Control
**Decision: Dummy `RooIgnoreController`** that allows everything except `.env` files (and similar sensitive defaults). Include a `TODO` comment for proper implementation later.
### 11.8 Command Prefix
**Decision: Use `kilo-code.new.autocomplete.*`** to be consistent with the existing extension naming convention.
### 11.9 NPM Dependencies
**Decision: Strip unused continuedev code first**, then install only what is actually needed.
### 11.10 Tree-Sitter WASM Bundling
**Decision: Bundle in `dist/`** via esbuild copy plugin.
### 11.11 Singleton vs Dependency Injection
**Decision: Deferred** — will be determined during implementation based on what works best with the existing `KiloConnectionService` / `KiloProvider` architecture.
@@ -1,409 +0,0 @@
# External Imports Investigation — `src/services/autocomplete/`
> Generated: 2026-02-12
>
> This document catalogs **every import** in the autocomplete module that references code
> **outside** of `src/services/autocomplete/`. Test files (`*.test.ts`, `*.spec.ts`, `__tests__/`)
> are excluded.
---
## Table of Contents
1. [Summary](#summary)
2. [VSCode API](#1-vscode-api)
3. [Internal Project Imports (non-continuedev)](#2-internal-project-imports-non-continuedev-files)
4. [Internal Project Imports (continuedev → outside autocomplete)](#3-internal-project-imports-continuedev-files-reaching-outside-autocomplete)
5. [Webview UI Imports](#4-webview-ui-imports)
6. [Monorepo Packages (`@roo-code/*`)](#5-monorepo-packages-roo-code)
7. [Third-Party npm Packages](#6-third-party-npm-packages)
8. [Node.js Built-in Modules](#7-nodejs-built-in-modules)
---
## Summary
| Category | Unique Modules | Total Import Sites |
| ---------------------------------------- | -------------------------------------------- | ------------------ |
| VSCode API | 1 (`vscode`) | 14 |
| Internal project (non-continuedev) | 12 distinct targets | 27 |
| Internal project (continuedev → outside) | 3 distinct targets | 3 |
| Webview UI | 1 (`PROVIDERS`) | 2 |
| Monorepo packages | 2 (`@roo-code/types`, `@roo-code/telemetry`) | 9 |
| Third-party npm | 16 distinct packages | 60+ |
| Node.js built-ins | 8 modules | 20+ |
---
## 1. VSCode API
All files import `* as vscode from "vscode"`.
| Source File | Symbols |
| --------------------------------------------------------------------------------------- | ------------- |
| `AutocompleteCodeActionProvider.ts` | `* as vscode` |
| `AutocompleteJetbrainsBridge.ts` | `* as vscode` |
| `AutocompleteServiceManager.ts` | `* as vscode` |
| `AutocompleteStatusBar.ts` | `* as vscode` |
| `index.ts` | `* as vscode` |
| `types.ts` | `* as vscode` |
| `chat-autocomplete/ChatTextAreaAutocomplete.ts` | `* as vscode` |
| `classic-auto-complete/AutocompleteInlineCompletionProvider.ts` | `* as vscode` |
| `classic-auto-complete/getProcessedSnippets.ts` | `* as vscode` |
| `context/VisibleCodeTracker.ts` | `* as vscode` |
| `continuedev/core/vscode-test-harness/src/VSCodeIde.ts` | `* as vscode` |
| `continuedev/core/vscode-test-harness/src/autocomplete/lsp.ts` | `* as vscode` |
| `continuedev/core/vscode-test-harness/src/autocomplete/recentlyEdited.ts` | `* as vscode` |
| `continuedev/core/vscode-test-harness/src/autocomplete/RecentlyVisitedRangesService.ts` | `* as vscode` |
---
## 2. Internal Project Imports (non-continuedev files)
These are imports from files **outside** the `src/services/autocomplete/` directory tree,
originating from the "Kilo Code" layer (not the continuedev fork).
### `src/core/` imports
| Source File | Module Path | Symbols |
| --------------------------------------------------------------- | ---------------------------------------------- | ---------------------------------- |
| `AutocompleteJetbrainsBridge.ts` | `../../core/webview/ClineProvider` | `{ ClineProvider }` |
| `AutocompleteJetbrainsBridge.ts` | `../../core/kilocode/wrapper` | `{ getKiloCodeWrapperProperties }` |
| `AutocompleteModel.ts` | `../../core/config/ProviderSettingsManager` | `{ ProviderSettingsManager }` |
| `AutocompleteServiceManager.ts` | `../../core/config/ContextProxy` | `{ ContextProxy }` |
| `AutocompleteServiceManager.ts` | `../../core/webview/ClineProvider` | `{ ClineProvider }` |
| `index.ts` | `../../core/webview/ClineProvider` | `{ ClineProvider }` |
| `types.ts` | `../../core/ignore/RooIgnoreController` | `{ RooIgnoreController }` |
| `chat-autocomplete/ChatTextAreaAutocomplete.ts` | `../../../core/config/ProviderSettingsManager` | `{ ProviderSettingsManager }` |
| `chat-autocomplete/handleChatCompletionRequest.ts` | `../../../core/webview/ClineProvider` | `{ ClineProvider }` |
| `classic-auto-complete/AutocompleteInlineCompletionProvider.ts` | `../../../core/ignore/RooIgnoreController` | `{ RooIgnoreController }` |
| `classic-auto-complete/AutocompleteInlineCompletionProvider.ts` | `../../../core/webview/ClineProvider` | `{ ClineProvider }` |
| `classic-auto-complete/getProcessedSnippets.ts` | `../../../core/ignore/RooIgnoreController` | `{ RooIgnoreController }` |
| `context/VisibleCodeTracker.ts` | `../../../core/ignore/RooIgnoreController` | `type { RooIgnoreController }` |
### `src/api/` imports
| Source File | Module Path | Symbols |
| ------------------------------------- | ----------------------------------------- | --------------------------------------------- |
| `AutocompleteModel.ts` | `../../api` | `{ ApiHandler, buildApiHandler, FimHandler }` |
| `AutocompleteModel.ts` | `../../api/providers` | `{ OpenRouterHandler }` |
| `AutocompleteModel.ts` | `../../api/providers/openrouter` | `{ CompletionUsage }` |
| `AutocompleteModel.ts` | `../../api/transform/stream` | `{ ApiStreamChunk }` |
| `AutocompleteModel.ts` | `../../api/providers/kilocode-openrouter` | `{ KilocodeOpenrouterHandler }` |
| `classic-auto-complete/HoleFiller.ts` | `../../../api/transform/stream` | `{ ApiStreamChunk }` |
### `src/i18n` imports
| Source File | Module Path | Symbols |
| ----------------------------------- | ------------ | ------- |
| `AutocompleteCodeActionProvider.ts` | `../../i18n` | `{ t }` |
| `AutocompleteServiceManager.ts` | `../../i18n` | `{ t }` |
| `AutocompleteStatusBar.ts` | `../../i18n` | `{ t }` |
### `src/shared/` imports
| Source File | Module Path | Symbols |
| --------------------------------------------------- | -------------------------------- | -------------------- |
| `chat-autocomplete/handleChatCompletionAccepted.ts` | `../../../shared/WebviewMessage` | `{ WebviewMessage }` |
| `chat-autocomplete/handleChatCompletionRequest.ts` | `../../../shared/WebviewMessage` | `{ WebviewMessage }` |
### `src/utils/` imports
| Source File | Module Path | Symbols |
| ------------------------------- | --------------------- | -------------------- |
| `context/VisibleCodeTracker.ts` | `../../../utils/path` | `{ toRelativePath }` |
### `src/services/mocking/` imports
| Source File | Module Path | Symbols |
| -------------------------------- | ----------------------------- | ---------------------- |
| `AutocompleteJetbrainsBridge.ts` | `../mocking/MockTextDocument` | `{ MockTextDocument }` |
---
## 3. Internal Project Imports (continuedev files reaching outside autocomplete)
These imports originate from `continuedev/` files but reach out of the autocomplete
directory tree entirely (via deeply nested `../../../../../../` paths).
| Source File | Module Path | Symbols |
| --------------------------------------- | ------------------------------------------------------- | ------------------------ |
| `continuedev/core/llm/llms/KiloCode.ts` | `../../../../../../shared/kilocode/headers` | `{ X_KILOCODE_VERSION }` |
| `continuedev/core/llm/llms/KiloCode.ts` | `../../../../../../shared/package` | `{ Package }` |
| `continuedev/core/llm/llms/KiloCode.ts` | `../../../../../../api/providers/kilocode/IFimProvider` | `{ IFimProvider }` |
---
## 4. Webview UI Imports
| Source File | Module Path | Symbols |
| -------------------------- | ------------------------------------------------------- | --------------- |
| `AutocompleteModel.ts` | `../../../webview-ui/src/components/settings/constants` | `{ PROVIDERS }` |
| `AutocompleteStatusBar.ts` | `../../../webview-ui/src/components/settings/constants` | `{ PROVIDERS }` |
---
## 5. Monorepo Packages (`@roo-code/*`)
### `@roo-code/types`
| Source File | Symbols |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `AutocompleteModel.ts` | `{ modelIdKeysByProvider, ProviderName }` |
| `AutocompleteServiceManager.ts` | `{ AutocompleteServiceSettings, TelemetryEventName }` |
| `AutocompleteStatusBar.ts` | `{ AUTOCOMPLETE_PROVIDER_MODELS, ProviderName }` |
| `classic-auto-complete/AutocompleteInlineCompletionProvider.ts` | `type { AutocompleteServiceSettings }` |
| `classic-auto-complete/AutocompleteTelemetry.ts` | `{ TelemetryEventName }` |
| `utils/kilocode-utils.ts` | `{ getKiloBaseUriFromToken, AUTOCOMPLETE_PROVIDER_MODELS, AutocompleteProviderKey }` |
| `continuedev/core/llm/llms/KiloCode.ts` | `{ getKiloUrlFromToken }` |
### `@roo-code/telemetry`
| Source File | Symbols |
| ------------------------------------------------ | ---------------------- |
| `AutocompleteServiceManager.ts` | `{ TelemetryService }` |
| `classic-auto-complete/AutocompleteTelemetry.ts` | `{ TelemetryService }` |
---
## 6. Third-Party npm Packages
### `openai` / `openai/*` (OpenAI SDK)
Used extensively in `continuedev/core/llm/` for LLM API adapters.
| Source File | Module Path | Symbols |
| ------------------------------------------------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `continuedev/core/llm/index.ts` | `openai/resources/index` | `{ ChatCompletionCreateParams }` |
| `continuedev/core/llm/llms/OpenAI.ts` | `openai/resources/index` | `{ ChatCompletionCreateParams, ChatCompletionMessageParam }` |
| `continuedev/core/llm/llms/OpenRouter.ts` | `openai/resources/index` | `{ ChatCompletionCreateParams }` |
| `continuedev/core/llm/openaiTypeConverters.ts` | `openai/resources/index` | `{ ChatCompletion, ChatCompletionChunk, ChatCompletionCreateParams, ChatCompletionMessageParam, CompletionCreateParams }` |
| `continuedev/core/llm/openai-adapters/apis/Anthropic.ts` | `openai/index` | `{ OpenAI }` |
| `continuedev/core/llm/openai-adapters/apis/Anthropic.ts` | `openai/resources/index` | `{ ChatCompletionChunk, ChatCompletionCreateParams, ... }` |
| `continuedev/core/llm/openai-adapters/apis/Anthropic.ts` | `openai/resources/index.js` | `{ ChatCompletionCreateParams }` |
| `continuedev/core/llm/openai-adapters/apis/AnthropicUtils.ts` | `openai/resources` | `{ ChatCompletionTool, ChatCompletionToolChoiceOption }` |
| `continuedev/core/llm/openai-adapters/apis/Azure.ts` | `openai/index` | `{ OpenAI }` |
| `continuedev/core/llm/openai-adapters/apis/Azure.ts` | `openai/resources/index` | `{ ChatCompletionChunk, ... }` |
| `continuedev/core/llm/openai-adapters/apis/base.ts` | `openai/resources/index` | `{ ChatCompletion, ChatCompletionChunk, ChatCompletionCreateParams, ... }` |
| `continuedev/core/llm/openai-adapters/apis/Bedrock.ts` | `openai/index` | `{ OpenAI }` |
| `continuedev/core/llm/openai-adapters/apis/Bedrock.ts` | `openai/resources/index` | `{ ChatCompletionChunk, ... }` |
| `continuedev/core/llm/openai-adapters/apis/Cohere.ts` | `openai/index` | `{ OpenAI }` |
| `continuedev/core/llm/openai-adapters/apis/Cohere.ts` | `openai/resources/index` | `{ ChatCompletion, ChatCompletionChunk, ... }` |
| `continuedev/core/llm/openai-adapters/apis/CometAPI.ts` | `openai/resources/index` | `{ ChatCompletion, ChatCompletionChunk, ... }` |
| `continuedev/core/llm/openai-adapters/apis/ContinueProxy.ts` | `openai/resources/index` | `{ ChatCompletion, ChatCompletionChunk, ... }` |
| `continuedev/core/llm/openai-adapters/apis/DeepSeek.ts` | `openai/resources/index` | `{ ChatCompletionChunk, Model }` |
| `continuedev/core/llm/openai-adapters/apis/Gemini.ts` | `openai/index` | `{ OpenAI }` |
| `continuedev/core/llm/openai-adapters/apis/Gemini.ts` | `openai/resources/index` | `{ ChatCompletionChunk, ... }` |
| `continuedev/core/llm/openai-adapters/apis/Inception.ts` | `openai/resources/index` | `{ ChatCompletionChunk, ... }` |
| `continuedev/core/llm/openai-adapters/apis/Jina.ts` | `openai/resources/index` | `{ ChatCompletionChunk, ... }` |
| `continuedev/core/llm/openai-adapters/apis/LlamaStack.ts` | `openai/resources/index` | `{ ChatCompletionChunk }` |
| `continuedev/core/llm/openai-adapters/apis/Mock.ts` | `openai/resources/index` | `{ ChatCompletionChunk, ... }` |
| `continuedev/core/llm/openai-adapters/apis/Moonshot.ts` | `openai/resources/index` | `{ ChatCompletionChunk, Model }` |
| `continuedev/core/llm/openai-adapters/apis/OpenAI.ts` | `openai/index` | `{ OpenAI }` |
| `continuedev/core/llm/openai-adapters/apis/OpenAI.ts` | `openai/resources/index` | `{ ChatCompletion, ChatCompletionChunk, ... }` |
| `continuedev/core/llm/openai-adapters/apis/OpenRouter.ts` | `openai/resources/index` | `{ ChatCompletionCreateParams }` |
| `continuedev/core/llm/openai-adapters/apis/OpenRouterCaching.ts` | `openai/resources/index` | `{ ChatCompletionCreateParams, ChatCompletionMessageParam }` |
| `continuedev/core/llm/openai-adapters/apis/Relace.ts` | `openai/resources/completions.mjs` | `{ Completion, CompletionUsage }` |
| `continuedev/core/llm/openai-adapters/apis/Relace.ts` | `openai/resources/index.mjs` | `{ ChatCompletion, ChatCompletionChunk, ... }` |
| `continuedev/core/llm/openai-adapters/apis/Relace.ts` | `openai/resources/models.mjs` | `{ Model }` |
| `continuedev/core/llm/openai-adapters/apis/VertexAI.ts` | `openai/resources/index` | `{ ChatCompletionChunk, ... }` |
| `continuedev/core/llm/openai-adapters/apis/WatsonX.ts` | `openai/index` | `{ OpenAI }` |
| `continuedev/core/llm/openai-adapters/apis/WatsonX.ts` | `openai/resources/index` | `{ ChatCompletionChunk, ... }` |
| `continuedev/core/llm/openai-adapters/apis/WatsonX.ts` | `openai/resources/index.js` | `{ ChatCompletionCreateParams }` |
| `continuedev/core/llm/openai-adapters/util.ts` | `openai/resources/index` | `{ ChatCompletionChunk, CompletionUsage }` |
| `continuedev/core/llm/openai-adapters/util.ts` | `openai/resources/index.js` | `{ ChatCompletion }` |
| `continuedev/core/llm/openai-adapters/util/emptyChatCompletion.ts` | `openai/resources/index` | `{ ChatCompletion }` |
| `continuedev/core/llm/openai-adapters/util/gemini-types.ts` | `openai/resources/index.mjs` | `{ ChatCompletionTool }` |
### `@anthropic-ai/sdk`
| Source File | Module Path | Symbols |
| ------------------------------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `continuedev/core/llm/openai-adapters/apis/Anthropic.ts` | `@anthropic-ai/sdk/resources` | `{ ContentBlock, ContentBlockDelta, MessageDelta, MessageStartEvent, RawContentBlockStartEvent, RawMessageDeltaEvent }` |
| `continuedev/core/llm/openai-adapters/apis/AnthropicCachingStrategies.ts` | `@anthropic-ai/sdk/resources` | `{ MessageCreateParams }` |
| `continuedev/core/llm/openai-adapters/apis/AnthropicUtils.ts` | `@anthropic-ai/sdk/resources` | `{ Base64ImageSource, MessageParam, Tool, ToolChoice }` |
| `continuedev/core/llm/openai-adapters/apis/OpenRouterCaching.ts` | `@anthropic-ai/sdk/resources` | `{ ContentBlockParam, MessageCreateParams, MessageParam }` |
### `@aws-sdk/*`
| Source File | Module Path | Symbols |
| ------------------------------------------------------ | --------------------------------- | -------------------------------------------------------------------------------------------- |
| `continuedev/core/llm/openai-adapters/apis/Bedrock.ts` | `@aws-sdk/client-bedrock-runtime` | `{ BedrockRuntimeClient, ConverseStreamCommand, InvokeModelWithResponseStreamCommand, ... }` |
| `continuedev/core/llm/openai-adapters/apis/Bedrock.ts` | `@aws-sdk/credential-providers` | `{ fromNodeProviderChain }` |
### `google-auth-library`
| Source File | Module Path | Symbols |
| ------------------------------------------------------- | --------------------- | --------------------------------------- |
| `continuedev/core/llm/openai-adapters/apis/VertexAI.ts` | `google-auth-library` | `{ AuthClient, GoogleAuth, JWT, auth }` |
### `zod`
| Source File | Module Path | Symbols |
| ------------------------------------------------------------ | ----------- | -------- |
| `AutocompleteJetbrainsBridge.ts` | `zod` | `{ z }` |
| `continuedev/core/llm/openai-adapters/apis/Azure.ts` | `zod` | `{ z }` |
| `continuedev/core/llm/openai-adapters/apis/ContinueProxy.ts` | `zod` | `{ z }` |
| `continuedev/core/llm/openai-adapters/apis/OpenAI.ts` | `zod` | `{ z }` |
| `continuedev/core/llm/openai-adapters/apis/Relace.ts` | `zod` | `{ z }` |
| `continuedev/core/llm/openai-adapters/index.ts` | `zod` | `{ z }` |
| `continuedev/core/llm/openai-adapters/types.ts` | `zod` | `* as z` |
### `web-tree-sitter`
| Source File | Module Path | Symbols |
| ----------------------------------------------------------------------------------- | ----------------- | ---------------------------------------------------- |
| `continuedev/core/index.d.ts` | `web-tree-sitter` | `Parser` (default) |
| `continuedev/core/autocomplete/util/ast.ts` | `web-tree-sitter` | `{ Node as SyntaxNode, Tree }` |
| `continuedev/core/autocomplete/context/root-path-context/RootPathContextService.ts` | `web-tree-sitter` | `{ Node as SyntaxNode, Query, Point }` |
| `continuedev/core/autocomplete/context/root-path-context/testUtils.ts` | `web-tree-sitter` | `Parser` (default) |
| `continuedev/core/autocomplete/context/static-context/StaticContextService.ts` | `web-tree-sitter` | `{ Node as SyntaxNode }` |
| `continuedev/core/autocomplete/context/static-context/tree-sitter-utils.ts` | `web-tree-sitter` | `{ Node as SyntaxNode, QueryMatch, Tree }` |
| `continuedev/core/autocomplete/context/static-context/types.ts` | `web-tree-sitter` | `{ Tree }` |
| `continuedev/core/util/treeSitter.ts` | `web-tree-sitter` | `type { Language, Node as SyntaxNode, Query, Tree }` |
| `continuedev/core/vscode-test-harness/src/autocomplete/lsp.ts` | `web-tree-sitter` | `type { Node as SyntaxNode }` |
### `diff`
| Source File | Module Path | Symbols |
| ------------------------------------------------------------------- | ----------- | --------------------------------------- |
| `continuedev/core/autocomplete/util/processSingleLineCompletion.ts` | `diff` | `* as Diff` |
| `continuedev/core/diff/myers.ts` | `diff` | `{ diffChars, diffLines, type Change }` |
### `fastest-levenshtein`
| Source File | Module Path | Symbols |
| ------------------------------------------------------ | --------------------- | -------------- |
| `continuedev/core/autocomplete/util/textSimilarity.ts` | `fastest-levenshtein` | `{ distance }` |
| `continuedev/core/diff/util.ts` | `fastest-levenshtein` | `{ distance }` |
### `js-tiktoken`
| Source File | Module Path | Symbols |
| ------------------------------------- | ------------- | ----------------------------------------------------- |
| `continuedev/core/llm/countTokens.ts` | `js-tiktoken` | `{ Tiktoken, encodingForModel as _encodingForModel }` |
### `lru-cache`
| Source File | Module Path | Symbols |
| --------------------------------------------------------------------------------------- | ----------- | -------------- |
| `continuedev/core/autocomplete/util/AutocompleteLruCacheInMem.ts` | `lru-cache` | `{ LRUCache }` |
| `continuedev/core/autocomplete/context/root-path-context/RootPathContextService.ts` | `lru-cache` | `{ LRUCache }` |
| `continuedev/core/vscode-test-harness/src/autocomplete/RecentlyVisitedRangesService.ts` | `lru-cache` | `{ LRUCache }` |
### `quick-lru`
| Source File | Module Path | Symbols |
| ----------------------------------------------------------- | ----------- | -------------------- |
| `continuedev/core/autocomplete/util/openedFilesLruCache.ts` | `quick-lru` | `QuickLRU` (default) |
### `ignore`
| Source File | Module Path | Symbols |
| ----------------------------------------------------- | ----------- | ------------------ |
| `continuedev/core/autocomplete/prefiltering/index.ts` | `ignore` | `ignore` (default) |
| `continuedev/core/indexing/ignore.ts` | `ignore` | `ignore` (default) |
### `dotenv`
| Source File | Module Path | Symbols |
| ----------------------------------------------- | ----------- | ------------------ |
| `continuedev/core/llm/openai-adapters/index.ts` | `dotenv` | `dotenv` (default) |
### `uri-js`
| Source File | Module Path | Symbols |
| ------------------------------------------------------- | ----------- | ---------- |
| `continuedev/core/vscode-test-harness/src/VSCodeIde.ts` | `uri-js` | `* as URI` |
### `vitest`
| Source File | Module Path | Symbols |
| ---------------------------------------------------------------------- | ----------- | ---------------- |
| `continuedev/core/autocomplete/context/root-path-context/testUtils.ts` | `vitest` | `{ expect, vi }` |
| `continuedev/core/autocomplete/filtering/test/util.ts` | `vitest` | `{ expect }` |
| `continuedev/core/test/vitest.setup.ts` | `vitest` | `{ beforeAll }` |
---
## 7. Node.js Built-in Modules
| Source File | Module | Symbols |
| ----------------------------------------------------------------------------------- | ------------- | ---------------------------------- |
| `AutocompleteServiceManager.ts` | `crypto` | `crypto` (default) |
| `continuedev/core/autocomplete/util/AutocompleteDebouncer.ts` | `node:crypto` | `{ randomUUID }` |
| `continuedev/core/llm/openai-adapters/apis/Bedrock.ts` | `node:crypto` | `{ randomUUID }` |
| `continuedev/core/autocomplete/context/root-path-context/RootPathContextService.ts` | `crypto` | `{ createHash }` |
| `continuedev/core/autocomplete/context/root-path-context/testUtils.ts` | `fs` | `fs` (default) |
| `continuedev/core/autocomplete/context/root-path-context/testUtils.ts` | `path` | `path` (default) |
| `continuedev/core/autocomplete/context/root-path-context/testUtils.ts` | `node:url` | `{ fileURLToPath }` |
| `continuedev/core/autocomplete/context/static-context/StaticContextService.ts` | `fs/promises` | `* as fs` |
| `continuedev/core/autocomplete/context/static-context/StaticContextService.ts` | `path` | `path` (default) |
| `continuedev/core/autocomplete/context/static-context/StaticContextService.ts` | `url` | `{ pathToFileURL }` |
| `continuedev/core/autocomplete/context/static-context/tree-sitter-utils.ts` | `fs/promises` | `* as fs` |
| `continuedev/core/indexing/ignore.ts` | `path` | `path` (default) |
| `continuedev/core/indexing/ignore.ts` | `url` | `{ fileURLToPath }` |
| `continuedev/core/test/testDir.ts` | `fs` | `fs` (default) |
| `continuedev/core/test/testDir.ts` | `os` | `os` (default) |
| `continuedev/core/test/testDir.ts` | `path` | `path` (default) |
| `continuedev/core/test/vitest.global-setup.ts` | `fs` | `fs` (default) |
| `continuedev/core/test/vitest.global-setup.ts` | `path` | `path` (default) |
| `continuedev/core/test/vitest.setup.ts` | `util` | `{ TextDecoder, TextEncoder }` |
| `continuedev/core/util/filesystem.ts` | `node:fs` | `* as fs` |
| `continuedev/core/util/filesystem.ts` | `node:url` | `{ fileURLToPath }` |
| `continuedev/core/util/paths.ts` | `fs` | `* as fs` |
| `continuedev/core/util/paths.ts` | `os` | `* as os` |
| `continuedev/core/util/paths.ts` | `path` | `* as path` |
| `continuedev/core/util/pathToUri.ts` | `url` | `{ fileURLToPath, pathToFileURL }` |
| `continuedev/core/util/treeSitter.ts` | `node:fs` | `fs` (default) |
| `continuedev/core/util/treeSitter.ts` | `path` | `path` (default) |
---
## Appendix: Unique External Dependency List
### npm packages (non-Node.js built-in)
```
@anthropic-ai/sdk
@aws-sdk/client-bedrock-runtime
@aws-sdk/credential-providers
@roo-code/telemetry
@roo-code/types
diff
dotenv
fastest-levenshtein
google-auth-library
ignore
js-tiktoken
lru-cache
openai
quick-lru
uri-js
vitest
web-tree-sitter
zod
```
### Internal project modules imported (outside autocomplete)
```
src/api (../../api)
src/api/providers (../../api/providers)
src/api/providers/kilocode-openrouter (../../api/providers/kilocode-openrouter)
src/api/providers/kilocode/IFimProvider (via continuedev KiloCode.ts)
src/api/providers/openrouter (../../api/providers/openrouter)
src/api/transform/stream (../../api/transform/stream)
src/core/config/ContextProxy (../../core/config/ContextProxy)
src/core/config/ProviderSettingsManager (../../core/config/ProviderSettingsManager)
src/core/ignore/RooIgnoreController (../../core/ignore/RooIgnoreController)
src/core/kilocode/wrapper (../../core/kilocode/wrapper)
src/core/webview/ClineProvider (../../core/webview/ClineProvider)
src/i18n (../../i18n)
src/services/mocking/MockTextDocument (../mocking/MockTextDocument)
src/shared/WebviewMessage (../../shared/WebviewMessage)
src/shared/kilocode/headers (via continuedev KiloCode.ts)
src/shared/package (via continuedev KiloCode.ts)
src/utils/path (../../utils/path)
webview-ui/src/components/settings/constants
```
@@ -1,456 +0,0 @@
# Internal Architecture of the Autocomplete Module
> Investigation date: 2026-02-12
> Scope: `src/services/autocomplete/` — all subdirectories and root-level files
---
## High-Level Architecture
```
┌──────────────────────────────────────────────────────────────────────┐
│ VS Code Extension Host │
│ │
│ index.ts ─► registerAutocompleteProvider() │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ AutocompleteServiceManager (singleton orchestrator) │ │
│ │ • settings, status bar, cost tracking, snooze, commands │ │
│ │ • owns AutocompleteModel + two completion strategies │ │
│ └────┬─────────────────────┬──────────────────────┬────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ Autocomplete│ │ Classic Auto- │ │ Chat Text Area │ │
│ │ Model │ │ Complete │ │ Autocomplete │ │
│ │ (LLM layer) │ │ (inline ghosts) │ │ (webview chat) │ │
│ └──────┬───┘ └──────┬──────────┘ └───────┬──────────────┘ │
│ │ │ │ │
│ │ ▼ ▼ │
│ │ ┌──────────────────────┐ ┌────────────────────┐ │
│ │ │ continuedev/ library │ │ context/ │ │
│ │ │ (forked Continue.dev)│ │ VisibleCodeTracker │ │
│ │ │ │ └────────────────────┘ │
│ │ │ • context retrieval │ │
│ │ │ • snippet gathering │ ┌────────────────────┐ │
│ │ │ • prompt templating │ │ utils/ │ │
│ │ │ • postprocessing │ │ kilocode-utils.ts │ │
│ │ │ • tree-sitter queries│ └────────────────────┘ │
│ │ │ • LLM providers │ │
│ │ └──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ External: src/api/ (ApiHandler, FimHandler, │ │
│ │ buildApiHandler, OpenRouterHandler, etc.) │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```
---
## 1. Root-Level Files
### [`index.ts`](../index.ts)
Entry point. Registers the `AutocompleteServiceManager`, JetBrains bridge, and all VS Code commands (`reload`, `generateSuggestions`, `disable`, `showIncompatibilityExtensionPopup`, code actions).
### [`AutocompleteServiceManager.ts`](../AutocompleteServiceManager.ts)
**Singleton orchestrator** for the entire module.
- Owns an [`AutocompleteModel`](../AutocompleteModel.ts) (LLM abstraction) and an [`AutocompleteInlineCompletionProvider`](../classic-auto-complete/AutocompleteInlineCompletionProvider.ts).
- Manages lifecycle: loads settings from `ContextProxy`, registers/unregisters the VS Code inline completion provider, handles snooze timers.
- Delegates manual "code suggestion" to the inline completion provider.
- Tracks session cost and completion count; forwards them to the status bar.
**External dependencies:**
- `vscode` API
- `@roo-code/types` (`AutocompleteServiceSettings`, `TelemetryEventName`)
- `@roo-code/telemetry` (`TelemetryService`)
- `../../core/config/ContextProxy`
- `../../core/webview/ClineProvider`
- `../../i18n` (`t()`)
### [`AutocompleteModel.ts`](../AutocompleteModel.ts)
**LLM abstraction layer** — bridges autocomplete requests to the Kilo Code API system.
- `reload(providerSettingsManager)`: Scans configured profiles to find a usable autocomplete provider. Supports dedicated `"autocomplete"` profiles and fallback to general profiles matching `AUTOCOMPLETE_PROVIDER_MODELS`.
- `supportsFim()`: Checks if the current API handler has a FIM endpoint.
- `generateFimResponse(prefix, suffix, onChunk)`: Streams a Fill-In-the-Middle completion via `FimHandler.streamFim()`.
- `generateResponse(system, user, onChunk)`: Streams a chat completion via `ApiHandler.createMessage()`.
- `hasValidCredentials()`, `getModelName()`, `getProviderDisplayName()`.
**External dependencies:**
- `../../api` (`ApiHandler`, `buildApiHandler`, `FimHandler`)
- `../../api/providers` (`OpenRouterHandler`, `KilocodeOpenrouterHandler`)
- `../../api/transform/stream` (`ApiStreamChunk`)
- `../../core/config/ProviderSettingsManager`
- `@roo-code/types` (`modelIdKeysByProvider`, `ProviderName`)
- `webview-ui/.../constants` (`PROVIDERS`)
### [`AutocompleteStatusBar.ts`](../AutocompleteStatusBar.ts)
VS Code status bar item showing autocomplete status (enabled/snoozed/disabled, model, cost).
### [`AutocompleteCodeActionProvider.ts`](../AutocompleteCodeActionProvider.ts)
VS Code code action provider (quick fix integration point).
### [`AutocompleteJetbrainsBridge.ts`](../AutocompleteJetbrainsBridge.ts)
Bridge for JetBrains IDE integration — proxies autocomplete requests from JetBrains to the classic auto-complete provider.
### [`types.ts`](../types.ts)
Central type definitions shared across subdirectories:
- `AutocompleteInput`, `AutocompleteOutcome`, `AutocompletePrompt` (discriminated union: `FimAutocompletePrompt | HoleFillerAutocompletePrompt`)
- `FillInAtCursorSuggestion`, `ResponseMetaData`, `CostTrackingCallback`
- `VisibleCodeContext`, `VisibleEditorInfo`, `VisibleRange`, `DiffInfo`
- `ChatCompletionRequest`, `ChatTextCompletionResult`
- Utility functions: `extractPrefixSuffix()`, `contextToAutocompleteInput()`
---
## 2. `continuedev/` — Forked Continue.dev Library
### Purpose
A **streamlined extraction** from the [Continue.dev](https://github.com/continuedev/continue) project, containing only autocomplete and NextEdit functionality. All GUI, chat, agents, and other features have been removed. It serves as a **TypeScript service library** providing:
1. **Autocomplete pipeline** — `CompletionProvider` orchestrates: prefiltering → context gathering → snippet retrieval → prompt templating → LLM streaming → stream filtering → postprocessing → caching.
2. **Context retrieval** — `ContextRetrievalService`, `ImportDefinitionsService`, `RootPathContextService`.
3. **Snippet gathering** — `getAllSnippets()` collects recently edited files, recently visited ranges, LSP definitions, clipboard, diffs.
4. **Prompt templating** — Model-specific FIM templates (`codestral`, `starcoder`, `deepseek`, etc.), prefix/suffix construction, token-limited rendering.
5. **Stream filtering** — `BracketMatchingService`, `charStream`/`lineStream` transforms, `StreamTransformPipeline`.
6. **Postprocessing** — bracket cleanup, `removePrefixOverlap`, completion formatting.
7. **Tree-sitter integration** — `.scm` query files for 15+ languages covering: code snippets, imports, root-path context, static context (hole/header/type queries), and tag queries.
8. **LLM providers** — `ILLM` interface with implementations for OpenAI, Mistral, OpenRouter, KiloCode, Mock.
9. **Diff engine** — Myers diff algorithm, streaming diff.
10. **Utility layer** — Token counting (llama tokenizer), LRU caching, logging service, debouncing, helper variables.
### Key Files
| File | Role |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| [`core/index.d.ts`](../continuedev/core/index.d.ts) | Core type definitions: `IDE`, `ILLM`, `TabAutocompleteOptions`, `Position`, `Range`, etc. |
| [`core/autocomplete/CompletionProvider.ts`](../continuedev/core/autocomplete/CompletionProvider.ts) | Main orchestrator (not used directly by classic-auto-complete; serves as reference) |
| [`core/autocomplete/context/ContextRetrievalService.ts`](../continuedev/core/autocomplete/context/) | Context gathering service |
| [`core/autocomplete/snippets/getAllSnippets.ts`](../continuedev/core/autocomplete/snippets/getAllSnippets.ts) | Collects all context snippets |
| [`core/autocomplete/templating/`](../continuedev/core/autocomplete/templating/) | Prompt construction, formatting, model-specific templates |
| [`core/autocomplete/postprocessing/`](../continuedev/core/autocomplete/postprocessing/) | Post-processing pipeline |
| [`core/autocomplete/util/HelperVars.ts`](../continuedev/core/autocomplete/util/HelperVars.ts) | Cursor context, pruned prefix/suffix computation |
| [`core/vscode-test-harness/src/VSCodeIde.ts`](../continuedev/core/vscode-test-harness/src/VSCodeIde.ts) | VS Code `IDE` interface implementation |
| [`core/vscode-test-harness/src/autocomplete/lsp.ts`](../continuedev/core/vscode-test-harness/src/autocomplete/lsp.ts) | LSP definition retrieval |
| [`core/vscode-test-harness/src/autocomplete/RecentlyVisitedRangesService.ts`](../continuedev/core/vscode-test-harness/src/autocomplete/RecentlyVisitedRangesService.ts) | Tracks recently visited code ranges |
| [`core/vscode-test-harness/src/autocomplete/recentlyEdited.ts`](../continuedev/core/vscode-test-harness/src/autocomplete/recentlyEdited.ts) | Tracks recently edited code ranges |
| [`core/llm/`](../continuedev/core/llm/) | LLM implementations (OpenAI, Mistral, etc.) |
| [`core/util/parameters.ts`](../continuedev/core/util/parameters.ts) | `DEFAULT_AUTOCOMPLETE_OPTS` |
| [`tree-sitter/`](../continuedev/tree-sitter/) | `.scm` query files for all supported languages |
### Self-containedness
The continuedev library is **largely self-contained** with its own:
- `IDE` interface (abstraction over VS Code/JetBrains)
- `ILLM` interface (abstraction over LLM providers)
- Tree-sitter integration
- Utility layer
It imports `web-tree-sitter` as an external npm dependency. The `VsCodeIde.ts` implementation within it imports `vscode` API directly.
---
## 3. `classic-auto-complete/` — Inline Code Completion Pipeline
### Purpose
The **primary code editor autocomplete** feature. Implements `vscode.InlineCompletionItemProvider` to show ghost text completions as the user types.
### Completion Pipeline Flow
```
VS Code triggers provideInlineCompletionItems()
│
▼
┌──────────────────────────────────────────┐
│ 1. Gate checks │
│ • Is auto-trigger enabled? │
│ • Has valid model/credentials? │
│ • Is file accessible (RooIgnore)? │
└──────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 2. Cache lookup (suggestions history) │
│ findMatchingSuggestion(): │
│ • exact match │
│ • partial_typing (user typed ahead) │
│ • backward_deletion (user backspaced) │
│ → If cache hit, return immediately │
└──────────┬───────────────────────────────┘
│ (cache miss)
▼
┌──────────────────────────────────────────┐
│ 3. Contextual skip │
│ shouldSkipAutocomplete(): │
│ • mid-word typing (len > 2) │
│ • at end of statement (;, }, )) │
│ → If skip, return empty │
└──────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 4. Prompt building │
│ Strategy chosen by model.supportsFim()│
│ │
│ FIM path (FimPromptBuilder): │
│ • getProcessedSnippets() → HelperVars │
│ • getAllSnippetsWithoutRace() → │
│ context, snippets, LSP defs │
│ • getTemplateForModel() → FIM format │
│ • compilePrefixSuffix() → formatted │
│ │
│ Chat path (HoleFiller): │
│ • Same context gathering │
│ • formatSnippets() → comment context │
│ • System prompt (hole-filler pattern) │
│ • {{FILL_HERE}} template │
└──────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 5. Debounced LLM request │
│ • Leading edge on first call │
│ • Adaptive delay (avg of recent │
│ latencies, 150ms–1000ms range) │
│ • Reuses covering pending requests │
└──────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 6. LLM call (via AutocompleteModel) │
│ FIM: model.generateFimResponse() │
│ → FimHandler.streamFim() │
│ Chat: model.generateResponse() │
│ → ApiHandler.createMessage() │
│ → Collects streaming chunks │
└──────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 7. Post-processing │
│ processSuggestion() → │
│ postprocessAutocompleteSuggestion(): │
│ a. continuedev postprocessCompletion() │
│ (prefix overlap removal, etc.) │
│ b. applyLanguageFilter() (markdown) │
│ c. suggestionConsideredDuplication() │
│ • prefix/suffix duplication │
│ • edge-line duplication │
│ • repetitive phrase detection │
│ • normalized complete-line check │
└──────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 8. Display logic │
│ • applyFirstLineOnly() — truncate │
│ multi-line if cursor is mid-line │
│ • stringToInlineCompletions() → │
│ vscode.InlineCompletionItem │
│ • Track in suggestionsHistory │
│ • Cost tracking callback │
│ • Telemetry (requested, returned, │
│ filtered, cache hit, accepted, │
│ unique shown, visibility tracking) │
└──────────────────────────────────────────┘
```
### Key Files
| File | Role |
| ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| [`AutocompleteInlineCompletionProvider.ts`](../classic-auto-complete/AutocompleteInlineCompletionProvider.ts) | Main provider: debouncing, cache, pipeline orchestration |
| [`FillInTheMiddle.ts`](../classic-auto-complete/FillInTheMiddle.ts) | `FimPromptBuilder` — builds FIM prompts using continuedev's templating |
| [`HoleFiller.ts`](../classic-auto-complete/HoleFiller.ts) | Chat-based completion with `{{FILL_HERE}}` / `<COMPLETION>` XML tag protocol |
| [`getProcessedSnippets.ts`](../classic-auto-complete/getProcessedSnippets.ts) | Orchestrates context gathering: `HelperVars`, `getAllSnippetsWithoutRace`, access filtering |
| [`contextualSkip.ts`](../classic-auto-complete/contextualSkip.ts) | Determines when to skip autocomplete (mid-word, end-of-statement) |
| [`uselessSuggestionFilter.ts`](../classic-auto-complete/uselessSuggestionFilter.ts) | Duplication detection, postprocessing pipeline integration |
| [`AutocompleteTelemetry.ts`](../classic-auto-complete/AutocompleteTelemetry.ts) | Telemetry events: requested, filtered, cache hit, LLM completed/failed, accepted, unique shown |
| [`language-filters/index.ts`](../classic-auto-complete/language-filters/index.ts) | Language-specific post-filters (currently: markdown) |
### External Dependencies (outside autocomplete module)
- `vscode` API (`InlineCompletionItemProvider`, `TextDocument`, `Position`, etc.)
- `../../api/transform/stream` (`ApiStreamChunk`)
- `../../core/ignore/RooIgnoreController` (file access filtering)
- `../../core/webview/ClineProvider`
- `@roo-code/types`, `@roo-code/telemetry`
---
## 4. `chat-autocomplete/` — Chat Text Area Autocomplete
### Purpose
Provides **autocomplete for the chat input text area** in the Kilo Code webview. When the user is typing a message in the chat panel, this module suggests completions for natural language text.
### Architecture
```
Webview sends "requestChatCompletion" message
│
▼
handleChatCompletionRequest()
│
├── Creates VisibleCodeTracker → captureVisibleCode()
├── Creates ChatTextAreaAutocomplete
│ │
│ ├── Initializes AutocompleteModel (same LLM layer)
│ ├── buildPrefix() — includes visible code context
│ │
│ ├── FIM path: model.generateFimResponse()
│ └── Chat path: model.generateResponse() with chat-specific prompts
│
│ cleanSuggestion():
│ ├── removePrefixOverlap()
│ ├── postprocessAutocompleteSuggestion()
│ ├── Filter code-looking suggestions (//, /*, #)
│ └── Truncate at first newline
│
└── Sends "chatCompletionResult" back to webview
```
### Communication
- **Request**: Webview → Extension: `{ type: "requestChatCompletion", text, requestId }`
- **Response**: Extension → Webview: `{ type: "chatCompletionResult", text, requestId }`
- **Acceptance**: Webview → Extension: `{ type: "chatCompletionAccepted", suggestionLength }`
### Key Files
| File | Role |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| [`ChatTextAreaAutocomplete.ts`](../chat-autocomplete/ChatTextAreaAutocomplete.ts) | Core logic: prompt building with visible code context, LLM call, cleaning |
| [`handleChatCompletionRequest.ts`](../chat-autocomplete/handleChatCompletionRequest.ts) | Message handler: creates tracker + autocomplete, sends result to webview |
| [`handleChatCompletionAccepted.ts`](../chat-autocomplete/handleChatCompletionAccepted.ts) | Telemetry handler for accepted suggestions |
### External Dependencies
- `../../core/webview/ClineProvider` (webview messaging)
- `../../core/config/ProviderSettingsManager`
- Reuses `AutocompleteTelemetry` and `postprocessAutocompleteSuggestion` from classic-auto-complete
---
## 5. `context/` — Visible Code Context
### Purpose
Captures what code is **actually visible** in the user's VS Code editor viewports (not just which files are open).
### [`VisibleCodeTracker.ts`](../context/VisibleCodeTracker.ts)
- Iterates `vscode.window.visibleTextEditors`
- For each editor: captures file path, language ID, visible line ranges, cursor position, selections
- Extracts diff information from git-scheme URIs
- Filters out files matching security patterns (`isSecurityConcern`) and `.kilocodeignore` rules
### External Dependencies
- `vscode` API (`window.visibleTextEditors`, `TextEditor`)
- `../../../utils/path` (`toRelativePath`)
- `../continuedev/core/indexing/ignore` (`isSecurityConcern`)
- `../../../core/ignore/RooIgnoreController`
---
## 6. `utils/` — Utility Functions
### [`kilocode-utils.ts`](../utils/kilocode-utils.ts)
- `checkKilocodeBalance(token, orgId)`: HTTP call to `/api/profile/balance` to verify positive balance.
- Re-exports `AUTOCOMPLETE_PROVIDER_MODELS` and `AutocompleteProviderKey` from `@roo-code/types`.
### External Dependencies
- `@roo-code/types` (`getKiloBaseUriFromToken`, `AUTOCOMPLETE_PROVIDER_MODELS`, `AutocompleteProviderKey`)
- Node.js `fetch`
---
## Key Interfaces and Abstractions
### Within the Module
| Interface | Defined In | Purpose |
| ----------------------------- | ---------- | --------------------------------------------------------------------------------------------- |
| `AutocompleteInput` | `types.ts` | Input for both FIM and hole-filler strategies |
| `AutocompletePrompt` | `types.ts` | Discriminated union: `FimAutocompletePrompt \| HoleFillerAutocompletePrompt` |
| `FillInAtCursorSuggestion` | `types.ts` | Result: `{ text, prefix, suffix }` |
| `ResponseMetaData` | `types.ts` | Cost/token tracking |
| `AutocompleteContextProvider` | `types.ts` | Bundles `ContextRetrievalService` + `VsCodeIde` + `AutocompleteModel` + `RooIgnoreController` |
| `CostTrackingCallback` | `types.ts` | `(cost, inputTokens, outputTokens) => void` |
| `VisibleCodeContext` | `types.ts` | Captured visible editor state |
### From continuedev
| Interface | Defined In | Purpose |
| ------------------------ | ---------------------------------------- | -------------------------------------------------------------- |
| `IDE` | `continuedev/core/index.d.ts` | IDE abstraction (file I/O, LSP, editor state) |
| `ILLM` | `continuedev/core/index.d.ts` | LLM abstraction (streamComplete, streamFim, chat, countTokens) |
| `TabAutocompleteOptions` | `continuedev/core/index.d.ts` | Autocomplete configuration options |
| `AutocompleteSnippet` | `continuedev/core/autocomplete/types.ts` | Context snippet with type discrimination |
### From External Code
| Interface/Class | From | Used For |
| ------------------------- | --------------------- | ---------------------------------- |
| `ApiHandler` | `src/api/` | Chat-based LLM streaming |
| `FimHandler` | `src/api/` | FIM completion streaming |
| `ProviderSettingsManager` | `src/core/config/` | Profile and provider configuration |
| `ContextProxy` | `src/core/config/` | Global state persistence |
| `ClineProvider` | `src/core/webview/` | Webview messaging, task access |
| `RooIgnoreController` | `src/core/ignore/` | File access filtering |
| `TelemetryService` | `@roo-code/telemetry` | Event tracking |
---
## Self-Contained vs. External Dependencies Summary
### Self-Contained (within `src/services/autocomplete/`)
- ✅ Completion pipeline logic (classic + chat)
- ✅ Contextual skip heuristics
- ✅ Suggestion history/cache management
- ✅ Duplication and uselessness filtering
- ✅ Language-specific filters
- ✅ Adaptive debouncing
- ✅ Visible code context capture
- ✅ Chat text area completion logic
- ✅ Telemetry event definitions
- ✅ continuedev library (context retrieval, snippet gathering, prompt templating, tree-sitter queries, postprocessing, LLM abstractions)
### Requires External Reconstruction
| Dependency | Source | What It Provides |
| --------------------------- | -------------------- | --------------------------------------------------------------------------------- |
| `ApiHandler` / `FimHandler` | `src/api/` | Actual LLM API calls (streaming FIM + chat) |
| `buildApiHandler()` | `src/api/` | Factory to create API handlers from profiles |
| `ProviderSettingsManager` | `src/core/config/` | Profile management, provider config |
| `ContextProxy` | `src/core/config/` | Persistent global state |
| `ClineProvider` | `src/core/webview/` | Webview messaging + task state |
| `RooIgnoreController` | `src/core/ignore/` | `.kilocodeignore` pattern matching |
| `vscode` API | VS Code | All editor integration (inline completions, status bar, commands, text documents) |
| `@roo-code/types` | `packages/types` | Shared type definitions, provider constants |
| `@roo-code/telemetry` | `packages/telemetry` | Telemetry event capture |
| `../../i18n` | `src/i18n/` | Localization |
| `../../utils/path` | `src/utils/` | Path utilities (`toRelativePath`) |
| `web-tree-sitter` | npm | Tree-sitter WASM parser (used by continuedev) |
| `PROVIDERS` constant | `webview-ui/` | Provider display name mapping |
@@ -1,412 +0,0 @@
# Investigation: VSCode Extension Integration Points for Autocomplete
This document captures every integration point between the autocomplete feature and the
VSCode extension infrastructure. It's intended for use when recreating the extension shell
in a standalone package.
---
## 1. package.json Contributions
### 1.1 Activation Events
```jsonc
// src/package.json lines 51-54
"activationEvents": [
"onLanguage", // activates for ANY language
"onStartupFinished" // activates after startup completes
]
```
There are no autocomplete-specific activation events. Both events are general-purpose.
### 1.2 Commands
Commands declared in `src/package.json` `contributes.commands`:
| Command ID | Title Key | Registered in Code? |
| ------------------------------------------------ | ------------------------------------------------ | ------------------------------------------ |
| `kilo-code.autocomplete.generateSuggestions` | `%autocomplete.commands.generateSuggestions%` | ✅ `src/services/autocomplete/index.ts:26` |
| `kilo-code.autocomplete.cancelSuggestions` | `%autocomplete.commands.cancelSuggestions%` | ❌ Never registered — placeholder |
| `kilo-code.autocomplete.applyCurrentSuggestions` | `%autocomplete.commands.applyCurrentSuggestion%` | ❌ Never registered — placeholder |
| `kilo-code.autocomplete.applyAllSuggestions` | `%autocomplete.commands.applyAllSuggestions%` | ❌ Never registered — placeholder |
| `kilo-code.autocomplete.goToNextSuggestion` | `%autocomplete.commands.goToNextSuggestion%` | ❌ Never registered — placeholder |
| `kilo-code.autocomplete.goToPreviousSuggestion` | `%autocomplete.commands.goToPreviousSuggestion%` | ❌ Never registered — placeholder |
Additional commands registered **programmatically** but NOT declared in package.json:
| Command ID | Registered in | Notes |
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------- |
| `kilo-code.autocomplete.reload` | `src/services/autocomplete/index.ts:16` | Reloads settings and model |
| `kilo-code.autocomplete.codeActionQuickFix` | `src/services/autocomplete/index.ts:21` | No-op stub |
| `kilo-code.autocomplete.showIncompatibilityExtensionPopup` | `src/services/autocomplete/index.ts:31` | Shows Copilot conflict dialog |
| `kilo-code.autocomplete.disable` | `src/services/autocomplete/index.ts:36` | Disables autocomplete |
| `kilocode.autocomplete.inline-completion.accepted` | `src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts:313` | Telemetry callback when suggestion accepted |
| `kilo-code.jetbrains.getInlineCompletions` | `src/services/autocomplete/AutocompleteJetbrainsBridge.ts:289` | JetBrains bridge |
### 1.3 Keybindings
From `src/package.json` `contributes.keybindings`:
| Command | Key | Mac | When Clause |
| ---------------------------------------------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kilo-code.autocomplete.cancelSuggestions` | `Escape` | same | `editorTextFocus && !editorTabMovesFocus && !inSnippetMode && kilocode.autocomplete.hasSuggestions` |
| `kilo-code.autocomplete.generateSuggestions` | `Ctrl+L` | `Cmd+L` | `editorTextFocus && !editorTabMovesFocus && !inSnippetMode && kilocode.autocomplete.enableSmartInlineTaskKeybinding && !github.copilot.completions.enabled` |
| `kilo-code.autocomplete.showIncompatibilityExtensionPopup` | `Ctrl+L` | `Cmd+L` | `editorTextFocus && !editorTabMovesFocus && !inSnippetMode && kilocode.autocomplete.enableSmartInlineTaskKeybinding && github.copilot.completions.enabled` |
**Note**: The `Escape` keybinding references `kilocode.autocomplete.hasSuggestions` which is
**never set** in code via `setContext`. This means the keybinding is currently inert
(would never activate).
### 1.4 Menus
There are **no menu contributions** for autocomplete commands. The autocomplete commands
do not appear in any editor/context, view/title, or other menu.
### 1.5 Configuration Settings
There are **no VSCode `configuration` contributions** for autocomplete in `package.json`.
Autocomplete settings are stored entirely in VSCode global state via `ContextProxy` under
the key `ghostServiceSettings`, not as workspace/user settings.
### 1.6 Code Actions
From `src/package.json`:
```jsonc
"codeActions": [{
"languages": ["*"],
"providedCodeActionKinds": ["vscode.CodeActionKind.QuickFix"]
}]
```
This is a general declaration. The autocomplete-specific code action provider is registered
programmatically in `src/services/autocomplete/index.ts:42-46`:
```typescript
vscode.languages.registerCodeActionsProvider("*", autocompleteManager.codeActionProvider, {
providedCodeActionKinds: Object.values(autocompleteManager.codeActionProvider.providedCodeActionKinds),
})
```
The `AutocompleteCodeActionProvider` (`src/services/autocomplete/AutocompleteCodeActionProvider.ts`)
provides a QuickFix action that triggers `kilo-code.autocomplete.generateSuggestions`.
---
## 2. Extension Activation & Service Initialization
### 2.1 Extension activation flow (`src/extension.ts`)
1. **Import**: `registerAutocompleteProvider` imported from `./services/autocomplete` (line 49)
2. **First-install defaults** (lines 400-414): On first install, autocomplete is enabled:
```typescript
const currentAutocompleteSettings = contextProxy.getValue("ghostServiceSettings")
await contextProxy.setValue("ghostServiceSettings", {
...currentAutocompleteSettings,
enableAutoTrigger: !kiloCodeWrapperJetbrains, // disabled for JetBrains
enableSmartInlineTaskKeybinding: true,
})
```
3. **Registration** (lines 512-520): Autocomplete is registered unless running as CLI:
```typescript
if (kiloCodeWrapperCode !== "cli") {
registerAutocompleteProvider(context, provider)
}
```
### 2.2 `registerAutocompleteProvider` (`src/services/autocomplete/index.ts`)
This function:
1. Creates `AutocompleteServiceManager` singleton
2. Registers JetBrains bridge via `registerAutocompleteJetbrainsBridge`
3. Registers 5 commands (reload, codeActionQuickFix, generateSuggestions, showIncompatibilityExtensionPopup, disable)
4. Registers `CodeActionsProvider` for all languages (`"*"`)
### 2.3 `AutocompleteServiceManager` initialization
On construction (`src/services/autocomplete/AutocompleteServiceManager.ts:37-61`):
1. Creates `AutocompleteModel` for provider/model management
2. Creates `AutocompleteCodeActionProvider` for QuickFix code actions
3. Creates `AutocompleteInlineCompletionProvider` for inline completions
4. Calls `this.load()` to initialize
### 2.4 `load()` method
On load (`src/services/autocomplete/AutocompleteServiceManager.ts:70-99`):
1. Reads `ghostServiceSettings` from `ContextProxy`
2. Sets context key `kilocode.autocomplete.enableSmartInlineTaskKeybinding` via `setContext`
3. Registers/unregisters `InlineCompletionItemProvider` based on `enableAutoTrigger` state
4. Updates status bar
5. Writes enriched settings (with provider/model info) back to `ContextProxy`
6. Posts state to webview
---
## 3. Context Keys (setContext)
| Context Key | Where Set | Purpose |
| ------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------- |
| `kilocode.autocomplete.enableSmartInlineTaskKeybinding` | `AutocompleteServiceManager.updateGlobalContext()` (line 286-290) | Controls whether `Cmd+L` / `Ctrl+L` keybinding is active |
| `kilocode.autocomplete.hasSuggestions` | ❌ Never set | Would control `Escape` keybinding — currently dead code |
---
## 4. Webview State Integration
### 4.1 State passing to webview (`src/core/webview/ClineProvider.ts`)
The `ghostServiceSettings` is destructured from global state and included in the
state object sent to the webview at:
- Line 2284: `ghostServiceSettings` extracted from state values
- Line 2480: `ghostServiceSettings: ghostServiceSettings` included in state to webview
- Line 2758: `ghostServiceSettings: stateValues.ghostServiceSettings` included in full state
### 4.2 Webview message handlers (`src/core/webview/webviewMessageHandler.ts`)
Three autocomplete-related message types are handled:
1. **`ghostServiceSettings`** (lines 1972-1982):
```typescript
case "ghostServiceSettings":
const validatedSettings = autocompleteServiceSettingsSchema.parse(message.values)
await updateGlobalState("ghostServiceSettings", validatedSettings)
await provider.postStateToWebview()
vscode.commands.executeCommand("kilo-code.autocomplete.reload")
```
This is the primary way the webview UI writes autocomplete settings.
2. **`snoozeAutocomplete`** (lines 1983-1989):
```typescript
case "snoozeAutocomplete":
if (typeof message.value === "number" && message.value > 0) {
await AutocompleteServiceManager.getInstance()?.snooze(message.value)
} else {
await AutocompleteServiceManager.getInstance()?.unsnooze()
}
```
3. **`requestChatCompletion`** (lines 3969-3975): Handles chat textarea FIM autocomplete.
4. **`chatCompletionAccepted`** (lines 3977-3979): Handles chat completion acceptance telemetry.
### 4.3 Autocomplete reload triggered from webview
The `kilo-code.autocomplete.reload` command is also triggered when API profiles change:
- `saveApiConfiguration` (line 2231)
- `upsertApiConfiguration` (lines 2294, 2303)
- `renameApiConfiguration` (line 2330)
- `deleteApiConfiguration` (line 2407)
---
## 5. Global State / ContextProxy
### 5.1 State key
The autocomplete feature uses a single global state key:
```
ghostServiceSettings: AutocompleteServiceSettings
```
Defined in `packages/types/src/global-settings.ts:230`:
```typescript
ghostServiceSettings: autocompleteServiceSettingsSchema
```
### 5.2 Schema (`packages/types/src/kilocode/kilocode.ts:9-19`)
```typescript
export const autocompleteServiceSettingsSchema = z
.object({
enableAutoTrigger: z.boolean().optional(),
enableSmartInlineTaskKeybinding: z.boolean().optional(),
enableChatAutocomplete: z.boolean().optional(),
provider: z.string().optional(),
model: z.string().optional(),
snoozeUntil: z.number().optional(),
hasKilocodeProfileWithNoBalance: z.boolean().optional(),
})
.optional()
```
### 5.3 Read/write patterns
- **Read**: `ContextProxy.instance.getGlobalState("ghostServiceSettings")`
- **Write**: `ContextProxy.instance.setValues({ ghostServiceSettings: ... })`
- **WebView write**: sends `{ type: "ghostServiceSettings", values: ... }` message
### 5.4 VSCode global state references
The key `ghostServiceSettings` is listed as a valid global state key for webview
communication in `packages/types/src/vscode-extension-host.ts:546` and `795`.
---
## 6. VSCode API Providers Registered
| Provider Type | Registration | Scope |
| ------------------------------ | ------------------------------------------------------------------------- | ---------------------------------------------------- |
| `InlineCompletionItemProvider` | `AutocompleteServiceManager.updateInlineCompletionProviderRegistration()` | `{ scheme: "file" }` — only for file-based documents |
| `CodeActionsProvider` | `src/services/autocomplete/index.ts:42` | `"*"` — all languages |
The inline completion provider is conditionally registered/disposed based on
`enableAutoTrigger` and snooze state.
---
## 7. Status Bar
`AutocompleteStatusBar` (`src/services/autocomplete/AutocompleteStatusBar.ts`):
- Alignment: `vscode.StatusBarAlignment.Right`, priority 100
- Shows/hides based on `enableAutoTrigger` setting
- Displays completion count, cost, provider info, and snoozed state
- Uses `$(kilo-logo)` codicon (custom icon font)
---
## 8. i18n / Localization
### 8.1 Package NLS keys (`src/package.nls.json` and locale variants)
Keys used in `package.json` command titles:
| Key | English Value |
| ---------------------------------------------- | ---------------------------------------------- |
| `autocomplete.commands.generateSuggestions` | Generate Suggested Edits |
| `autocomplete.commands.displaySuggestions` | Display Suggested Edits |
| `autocomplete.commands.cancelSuggestions` | Cancel Suggested Edits |
| `autocomplete.commands.applyCurrentSuggestion` | Apply Current Suggested Edit |
| `autocomplete.commands.applyAllSuggestions` | Apply All Suggested Edits |
| `autocomplete.commands.goToNextSuggestion` | Go To Next Suggestion |
| `autocomplete.commands.goToPreviousSuggestion` | Go To Previous Suggestion |
| `autocomplete.input.title` | Press 'Enter' to confirm or 'Escape' to cancel |
| `autocomplete.input.placeholder` | Describe what you want to do... |
Translated in 20+ locale files: `src/package.nls.{de,fr,es,it,ja,ko,nl,pl,pt-BR,ru,sk,cs,uk,zh-CN,zh-TW,ar,ca,hi,id,th,tr,vi}.json`.
### 8.2 Runtime i18n keys (src/i18n/locales/en/kilocode.json)
Namespace: `kilocode:autocomplete.*`
```jsonc
{
"autocomplete": {
"statusBar": {
"enabled": "$(kilo-logo) Autocomplete",
"snoozed": "snoozed",
"warning": "$(warning) Autocomplete",
"tooltip": {
"basic": "Kilo Code Autocomplete",
"disabled": "Kilo Code Autocomplete (disabled)",
"noCredits": "...",
"noUsableProvider": "...",
"sessionTotal": "Session total cost:",
"provider": "Provider:",
"model": "Model:",
"profile": "Profile: ",
"defaultProfile": "Default",
"completionSummary": "Performed {{count}} completions between {{startTime}} and {{endTime}}, for a total cost of {{cost}}.",
"providerInfo": "Autocompletions provided by {{model}} via {{provider}}.",
},
"cost": {
"zero": "$0.00",
"lessThanCent": "<$0.01",
},
},
"toggleMessage": "Kilo Code Autocomplete {{status}}",
"progress": {
"title": "Kilo Code",
"analyzing": "Analyzing your code...",
"generating": "Generating suggested edits...",
"processing": "Processing suggested edits...",
"showing": "Displaying suggested edits...",
},
"input": {
"title": "Kilo Code: Quick Task",
"placeholder": "e.g., 'refactor this function to be more efficient'",
},
"commands": {
"generateSuggestions": "Kilo Code: Generate Suggested Edits",
"displaySuggestions": "Display Suggested Edits",
"cancelSuggestions": "Cancel Suggested Edits",
"applyCurrentSuggestion": "Apply Current Suggested Edit",
"applyAllSuggestions": "Apply All Suggested Edits",
"category": "Kilo Code",
},
"codeAction": {
"title": "Kilo Code: Suggested Edits",
},
"chatParticipant": {
"fullName": "Kilo Code Agent",
"name": "Agent",
"description": "I can help you with quick tasks and suggested edits.",
},
"incompatibilityExtensionPopup": {
"message": "The Kilo Code Autocomplete is being blocked by a conflict with GitHub Copilot. To fix this, you must disable Copilot's inline suggestions.",
"disableCopilot": "Disable Copilot",
"disableInlineAssist": "Disable Autocomplete",
},
},
}
```
Translated in all locale files under `src/i18n/locales/{locale}/kilocode.json`.
---
## 9. Telemetry Events
| Event | Where Used |
| -------------------------------------------- | -------------------------------------------------------------------- |
| `TelemetryEventName.INLINE_ASSIST_AUTO_TASK` | `AutocompleteServiceManager.codeSuggestion()` |
| `TelemetryEventName.GHOST_SERVICE_DISABLED` | `AutocompleteServiceManager.disable()` |
| Accept suggestion telemetry | `AutocompleteInlineCompletionProvider` via accepted command callback |
---
## 10. Dependencies on Host Extension
The autocomplete service depends on:
1. **`ClineProvider`** — for `providerSettingsManager` (API provider configs), `postStateToWebview()`
2. **`ContextProxy`** — singleton for global state read/write
3. **`vscode.ExtensionContext`** — for `subscriptions` (disposable management), `globalState`
4. **Webview messaging** — bidirectional communication for settings changes
5. **Custom icon font** — `$(kilo-logo)` codicon from `assets/icons/kilo-icon-font.woff2`
---
## 11. Legacy: Ghost Service
The code refers to `ghostServiceSettings` and `GhostServiceManager` in
`src/services/ghost/` — this appears to be the original/predecessor copy.
The current active autocomplete uses `src/services/autocomplete/` which is
a refactored version. The `ghost` name persists in the global state key
`ghostServiceSettings` for backwards compatibility.
---
## 12. Summary: What a New Extension Would Need
To recreate the autocomplete as a standalone extension:
1. **package.json**: 6 declared commands + keybindings + code action declaration
2. **Activation**: `onLanguage` + `onStartupFinished`, register providers on activate
3. **Providers**: `InlineCompletionItemProvider` (file scheme), `CodeActionsProvider` (all langs)
4. **Context keys**: `kilocode.autocomplete.enableSmartInlineTaskKeybinding` (via setContext)
5. **Status bar**: Right-aligned status bar item with custom icon
6. **State**: Single `ghostServiceSettings` object in global state (or a new key)
7. **Webview communication**: Message types `ghostServiceSettings`, `snoozeAutocomplete`, `requestChatCompletion`, `chatCompletionAccepted`
8. **i18n**: ~10 NLS keys in package.nls.json + ~30 runtime keys in kilocode.json
9. **Telemetry**: 2 primary event types
@@ -1,49 +0,0 @@
// ar runtime translations for autocomplete
export const dict = {
"kilocode:autocomplete.statusBar.enabled": "$(kilo-logo) Autocomplete",
"kilocode:autocomplete.statusBar.snoozed": "متوقف مؤقتاً",
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (معطل)",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**لم يتم تكوين نموذج الإكمال التلقائي**\n\nلتمكين الإكمال التلقائي، أضف ملفًا شخصيًا مع أحد هذه المزودين المدعومين: {{providers}}.\n\n[فتح الإعدادات]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "إجمالي تكلفة الجلسة:",
"kilocode:autocomplete.statusBar.tooltip.provider": "المزود:",
"kilocode:autocomplete.statusBar.tooltip.model": "النموذج:",
"kilocode:autocomplete.statusBar.tooltip.profile": "الملف الشخصي: ",
"kilocode:autocomplete.statusBar.tooltip.defaultProfile": "افتراضي",
"kilocode:autocomplete.statusBar.tooltip.completionSummary":
"تم إجراء {{count}} إكمال بين {{startTime}} و {{endTime}}، بتكلفة إجمالية {{cost}}.",
"kilocode:autocomplete.statusBar.tooltip.providerInfo":
"يتم توفير الإكمال التلقائي بواسطة {{model}} عبر {{provider}}.",
"kilocode:autocomplete.statusBar.cost.zero": "$0.00",
"kilocode:autocomplete.statusBar.cost.lessThanCent": "<$0.01",
"kilocode:autocomplete.toggleMessage": "Kilo Code Autocomplete {{status}}",
"kilocode:autocomplete.progress.title": "Kilo Code",
"kilocode:autocomplete.progress.analyzing": "تحليل الكود الخاص بك...",
"kilocode:autocomplete.progress.generating": "إنشاء التعديلات المقترحة...",
"kilocode:autocomplete.progress.processing": "معالجة التعديلات المقترحة...",
"kilocode:autocomplete.progress.showing": "عرض التعديلات المقترحة...",
"kilocode:autocomplete.input.title": "Kilo Code: مهمة سريعة",
"kilocode:autocomplete.input.placeholder": "مثال، 'أعد هيكلة هذه الدالة لتكون أكثر كفاءة'",
"kilocode:autocomplete.commands.generateSuggestions": "Kilo Code: إنشاء التعديلات المقترحة",
"kilocode:autocomplete.commands.displaySuggestions": "عرض التعديلات المقترحة",
"kilocode:autocomplete.commands.cancelSuggestions": "إلغاء التعديلات المقترحة",
"kilocode:autocomplete.commands.applyCurrentSuggestion": "تطبيق التعديل المقترح الحالي",
"kilocode:autocomplete.commands.applyAllSuggestions": "تطبيق جميع التعديلات المقترحة",
"kilocode:autocomplete.commands.category": "Kilo Code",
"kilocode:autocomplete.codeAction.title": "Kilo Code: التعديلات المقترحة",
"kilocode:autocomplete.chatParticipant.fullName": "Kilo Code Agent",
"kilocode:autocomplete.chatParticipant.name": "Agent",
"kilocode:autocomplete.chatParticipant.description": "يمكنني مساعدتك في المهام السريعة والتعديلات المقترحة.",
"kilocode:autocomplete.incompatibilityExtensionPopup.message":
"يتم حظر Kilo Code Autocomplete بسبب تعارض مع GitHub Copilot. لإصلاح هذا، يجب عليك تعطيل اقتراحات Copilot المضمنة.",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "تعطيل Copilot",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "تعطيل الإكمال التلقائي",
"kilocode:autocomplete.creditsExhausted.message":
"تم إيقاف Kilo Code Autocomplete مؤقتًا لأن حسابك لا يحتوي على رصيد متبقٍ. أضف رصيدًا لاستئناف الإكمال التلقائي.",
"kilocode:autocomplete.creditsExhausted.addCredits": "إضافة رصيد",
"kilocode:autocomplete.authError.message":
"تم إيقاف Kilo Code Autocomplete مؤقتًا بسبب خطأ في المصادقة. يرجى تسجيل الدخول مرة أخرى.",
}
@@ -1,49 +0,0 @@
// ca runtime translations for autocomplete
export const dict = {
"kilocode:autocomplete.statusBar.enabled": "$(kilo-logo) Autocomplete",
"kilocode:autocomplete.statusBar.snoozed": "pausat",
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (desactivat)",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**No s'ha configurat cap model d'autocompletat**\n\nPer habilitar l'autocompletat, afegeix un perfil amb un d'aquests proveïdors compatibles: {{providers}}.\n\n[Obrir Configuració]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Cost total de la sessió:",
"kilocode:autocomplete.statusBar.tooltip.provider": "Proveïdor:",
"kilocode:autocomplete.statusBar.tooltip.model": "Model:",
"kilocode:autocomplete.statusBar.tooltip.profile": "Perfil: ",
"kilocode:autocomplete.statusBar.tooltip.defaultProfile": "Per defecte",
"kilocode:autocomplete.statusBar.tooltip.completionSummary":
"S'han realitzat {{count}} completacions entre {{startTime}} i {{endTime}}, amb un cost total de {{cost}}.",
"kilocode:autocomplete.statusBar.tooltip.providerInfo":
"Les completacions automàtiques són proporcionades per {{model}} via {{provider}}.",
"kilocode:autocomplete.statusBar.cost.zero": "$0.00",
"kilocode:autocomplete.statusBar.cost.lessThanCent": "<$0.01",
"kilocode:autocomplete.toggleMessage": "Kilo Code Autocomplete {{status}}",
"kilocode:autocomplete.progress.title": "Kilo Code",
"kilocode:autocomplete.progress.analyzing": "Analitzant el teu codi...",
"kilocode:autocomplete.progress.generating": "Generant edicions suggerides...",
"kilocode:autocomplete.progress.processing": "Processant edicions suggerides...",
"kilocode:autocomplete.progress.showing": "Mostrant edicions suggerides...",
"kilocode:autocomplete.input.title": "Kilo Code: Tasca Ràpida",
"kilocode:autocomplete.input.placeholder": "p. ex., 'refactoritza aquesta funció per ser més eficient'",
"kilocode:autocomplete.commands.generateSuggestions": "Kilo Code: Generar Edicions Suggerides",
"kilocode:autocomplete.commands.displaySuggestions": "Mostrar Edicions Suggerides",
"kilocode:autocomplete.commands.cancelSuggestions": "Cancel·lar Edicions Suggerides",
"kilocode:autocomplete.commands.applyCurrentSuggestion": "Aplicar Edició Suggerida Actual",
"kilocode:autocomplete.commands.applyAllSuggestions": "Aplicar Totes les Edicions Suggerides",
"kilocode:autocomplete.commands.category": "Kilo Code",
"kilocode:autocomplete.codeAction.title": "Kilo Code: Edicions Suggerides",
"kilocode:autocomplete.chatParticipant.fullName": "Kilo Code Agent",
"kilocode:autocomplete.chatParticipant.name": "Agent",
"kilocode:autocomplete.chatParticipant.description": "Puc ajudar-te amb tasques ràpides i edicions suggerides.",
"kilocode:autocomplete.incompatibilityExtensionPopup.message":
"El Kilo Code Autocomplete està sent bloquejat per un conflicte amb GitHub Copilot. Per solucionar això, has de desactivar els suggeriments en línia de Copilot.",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Desactivar Copilot",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Desactivar Autocomplete",
"kilocode:autocomplete.creditsExhausted.message":
"Kilo Code Autocomplete s'ha pausat perquè el teu compte no té crèdits restants. Afegeix crèdits per reprendre l'autocompletat.",
"kilocode:autocomplete.creditsExhausted.addCredits": "Afegir crèdits",
"kilocode:autocomplete.authError.message":
"Kilo Code Autocomplete s'ha pausat a causa d'un error d'autenticació. Si us plau, torna a iniciar sessió.",
}
@@ -1,49 +0,0 @@
// cs runtime translations for autocomplete
export const dict = {
"kilocode:autocomplete.statusBar.enabled": "$(kilo-logo) Autocomplete",
"kilocode:autocomplete.statusBar.snoozed": "pozastaveno",
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (zakázáno)",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Není nakonfigurován žádný model automatického doplňování**\n\nPro povolení automatického doplňování přidej profil s jedním z těchto podporovaných poskytovatelů: {{providers}}.\n\n[Otevřít Nastavení]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Celkové náklady relace:",
"kilocode:autocomplete.statusBar.tooltip.provider": "Poskytovatel:",
"kilocode:autocomplete.statusBar.tooltip.model": "Model:",
"kilocode:autocomplete.statusBar.tooltip.profile": "Profil: ",
"kilocode:autocomplete.statusBar.tooltip.defaultProfile": "Výchozí",
"kilocode:autocomplete.statusBar.tooltip.completionSummary":
"Provedeno {{count}} dokončení mezi {{startTime}} a {{endTime}}, s celkovými náklady {{cost}}.",
"kilocode:autocomplete.statusBar.tooltip.providerInfo":
"Automatické dokončování poskytuje {{model}} přes {{provider}}.",
"kilocode:autocomplete.statusBar.cost.zero": "$0.00",
"kilocode:autocomplete.statusBar.cost.lessThanCent": "<$0.01",
"kilocode:autocomplete.toggleMessage": "Kilo Code Autocomplete {{status}}",
"kilocode:autocomplete.progress.title": "Kilo Code",
"kilocode:autocomplete.progress.analyzing": "Analyzuji tvůj kód...",
"kilocode:autocomplete.progress.generating": "Generuji navrhované úpravy...",
"kilocode:autocomplete.progress.processing": "Zpracovávám navrhované úpravy...",
"kilocode:autocomplete.progress.showing": "Zobrazuji navrhované úpravy...",
"kilocode:autocomplete.input.title": "Kilo Code: Rychlý úkol",
"kilocode:autocomplete.input.placeholder": "např. 'refaktoruj tuto funkci, aby byla efektivnější'",
"kilocode:autocomplete.commands.generateSuggestions": "Kilo Code: Generovat navrhované úpravy",
"kilocode:autocomplete.commands.displaySuggestions": "Zobrazit navrhované úpravy",
"kilocode:autocomplete.commands.cancelSuggestions": "Zrušit navrhované úpravy",
"kilocode:autocomplete.commands.applyCurrentSuggestion": "Použít aktuální navrhovanou úpravu",
"kilocode:autocomplete.commands.applyAllSuggestions": "Použít všechny navrhované úpravy",
"kilocode:autocomplete.commands.category": "Kilo Code",
"kilocode:autocomplete.codeAction.title": "Kilo Code: Navrhované úpravy",
"kilocode:autocomplete.chatParticipant.fullName": "Kilo Code Agent",
"kilocode:autocomplete.chatParticipant.name": "Agent",
"kilocode:autocomplete.chatParticipant.description": "Mohu ti pomoci s rychlými úkoly a navrženými úpravami.",
"kilocode:autocomplete.incompatibilityExtensionPopup.message":
"Kilo Code Autocomplete je blokováno konfliktem s GitHub Copilot. Pro vyřešení tohoto problému musíš zakázat inline návrhy Copilot.",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Zakázat Copilot",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Zakázat Autocomplete",
"kilocode:autocomplete.creditsExhausted.message":
"Kilo Code Autocomplete bylo pozastaveno, protože na tvém účtu nezbyly žádné kredity. Přidej kredity pro obnovení automatického doplňování.",
"kilocode:autocomplete.creditsExhausted.addCredits": "Přidat kredity",
"kilocode:autocomplete.authError.message":
"Kilo Code Autocomplete bylo pozastaveno kvůli chybě ověření. Prosím, přihlas se znovu.",
}
@@ -1,50 +0,0 @@
// de runtime translations for autocomplete
export const dict = {
"kilocode:autocomplete.statusBar.enabled": "$(kilo-logo) Autocomplete",
"kilocode:autocomplete.statusBar.snoozed": "pausiert",
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (deaktiviert)",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Kein Autocomplete-Modell konfiguriert**\n\nUm Autocomplete zu aktivieren, füge ein Profil mit einem dieser unterstützten Anbieter hinzu: {{providers}}.\n\n[Einstellungen öffnen]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Sitzungsgesamtkosten:",
"kilocode:autocomplete.statusBar.tooltip.provider": "Anbieter:",
"kilocode:autocomplete.statusBar.tooltip.model": "Modell:",
"kilocode:autocomplete.statusBar.tooltip.profile": "Profil: ",
"kilocode:autocomplete.statusBar.tooltip.defaultProfile": "Standard",
"kilocode:autocomplete.statusBar.tooltip.completionSummary":
"{{count}} Vervollständigungen zwischen {{startTime}} und {{endTime}} durchgeführt, für Gesamtkosten von {{cost}}.",
"kilocode:autocomplete.statusBar.tooltip.providerInfo":
"Autovervollständigungen bereitgestellt von {{model}} über {{provider}}.",
"kilocode:autocomplete.statusBar.cost.zero": "$0.00",
"kilocode:autocomplete.statusBar.cost.lessThanCent": "<$0.01",
"kilocode:autocomplete.toggleMessage": "Kilo Code Autocomplete {{status}}",
"kilocode:autocomplete.progress.title": "Kilo Code",
"kilocode:autocomplete.progress.analyzing": "Analysiere deinen Code...",
"kilocode:autocomplete.progress.generating": "Generiere Bearbeitungsvorschläge...",
"kilocode:autocomplete.progress.processing": "Verarbeite Bearbeitungsvorschläge...",
"kilocode:autocomplete.progress.showing": "Zeige Bearbeitungsvorschläge...",
"kilocode:autocomplete.input.title": "Kilo Code: Schnellaufgabe",
"kilocode:autocomplete.input.placeholder": "z.B. 'refaktoriere diese Funktion für mehr Effizienz'",
"kilocode:autocomplete.commands.generateSuggestions": "Kilo Code: Bearbeitungsvorschläge generieren",
"kilocode:autocomplete.commands.displaySuggestions": "Bearbeitungsvorschläge anzeigen",
"kilocode:autocomplete.commands.cancelSuggestions": "Bearbeitungsvorschläge abbrechen",
"kilocode:autocomplete.commands.applyCurrentSuggestion": "Aktuellen Bearbeitungsvorschlag anwenden",
"kilocode:autocomplete.commands.applyAllSuggestions": "Alle Bearbeitungsvorschläge anwenden",
"kilocode:autocomplete.commands.category": "Kilo Code",
"kilocode:autocomplete.codeAction.title": "Kilo Code: Bearbeitungsvorschläge",
"kilocode:autocomplete.chatParticipant.fullName": "Kilo Code Agent",
"kilocode:autocomplete.chatParticipant.name": "Agent",
"kilocode:autocomplete.chatParticipant.description":
"Ich kann dir bei Schnellaufgaben und Bearbeitungsvorschlägen helfen.",
"kilocode:autocomplete.incompatibilityExtensionPopup.message":
"Das Kilo Code Autocomplete wird durch einen Konflikt mit GitHub Copilot blockiert. Um dies zu beheben, musst du Copilots Inline-Vorschläge deaktivieren.",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Copilot deaktivieren",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Autocomplete deaktivieren",
"kilocode:autocomplete.creditsExhausted.message":
"Kilo Code Autocomplete wurde pausiert, weil dein Konto kein Guthaben mehr hat. Füge Guthaben hinzu, um Autocomplete fortzusetzen.",
"kilocode:autocomplete.creditsExhausted.addCredits": "Guthaben hinzufügen",
"kilocode:autocomplete.authError.message":
"Kilo Code Autocomplete wurde aufgrund eines Authentifizierungsfehlers pausiert. Bitte melde dich erneut an.",
}
@@ -1,49 +0,0 @@
// es runtime translations for autocomplete
export const dict = {
"kilocode:autocomplete.statusBar.enabled": "$(kilo-logo) Autocomplete",
"kilocode:autocomplete.statusBar.snoozed": "pausado",
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (deshabilitado)",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**No hay modelo de autocompletado configurado**\n\nPara habilitar el autocompletado, añade un perfil con uno de estos proveedores compatibles: {{providers}}.\n\n[Abrir Configuración]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Costo total de la sesión:",
"kilocode:autocomplete.statusBar.tooltip.provider": "Proveedor:",
"kilocode:autocomplete.statusBar.tooltip.model": "Modelo:",
"kilocode:autocomplete.statusBar.tooltip.profile": "Perfil: ",
"kilocode:autocomplete.statusBar.tooltip.defaultProfile": "Predeterminado",
"kilocode:autocomplete.statusBar.tooltip.completionSummary":
"Se realizaron {{count}} completaciones entre {{startTime}} y {{endTime}}, por un costo total de {{cost}}.",
"kilocode:autocomplete.statusBar.tooltip.providerInfo":
"Autocompletaciones proporcionadas por {{model}} a través de {{provider}}.",
"kilocode:autocomplete.statusBar.cost.zero": "$0.00",
"kilocode:autocomplete.statusBar.cost.lessThanCent": "<$0.01",
"kilocode:autocomplete.toggleMessage": "Kilo Code Autocomplete {{status}}",
"kilocode:autocomplete.progress.title": "Kilo Code",
"kilocode:autocomplete.progress.analyzing": "Analizando tu código...",
"kilocode:autocomplete.progress.generating": "Generando ediciones sugeridas...",
"kilocode:autocomplete.progress.processing": "Procesando ediciones sugeridas...",
"kilocode:autocomplete.progress.showing": "Mostrando ediciones sugeridas...",
"kilocode:autocomplete.input.title": "Kilo Code: Tarea Rápida",
"kilocode:autocomplete.input.placeholder": "ej., 'refactoriza esta función para que sea más eficiente'",
"kilocode:autocomplete.commands.generateSuggestions": "Kilo Code: Generar Ediciones Sugeridas",
"kilocode:autocomplete.commands.displaySuggestions": "Mostrar Ediciones Sugeridas",
"kilocode:autocomplete.commands.cancelSuggestions": "Cancelar Ediciones Sugeridas",
"kilocode:autocomplete.commands.applyCurrentSuggestion": "Aplicar Edición Sugerida Actual",
"kilocode:autocomplete.commands.applyAllSuggestions": "Aplicar Todas las Ediciones Sugeridas",
"kilocode:autocomplete.commands.category": "Kilo Code",
"kilocode:autocomplete.codeAction.title": "Kilo Code: Ediciones Sugeridas",
"kilocode:autocomplete.chatParticipant.fullName": "Kilo Code Agent",
"kilocode:autocomplete.chatParticipant.name": "Agent",
"kilocode:autocomplete.chatParticipant.description": "Puedo ayudarte con tareas rápidas y ediciones sugeridas.",
"kilocode:autocomplete.incompatibilityExtensionPopup.message":
"El Kilo Code Autocomplete está siendo bloqueado por un conflicto con GitHub Copilot. Para solucionarlo, debes desactivar las sugerencias en línea de Copilot.",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Desactivar Copilot",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Desactivar Autocomplete",
"kilocode:autocomplete.creditsExhausted.message":
"Kilo Code Autocomplete se ha pausado porque tu cuenta no tiene créditos restantes. Añade créditos para reanudar el autocompletado.",
"kilocode:autocomplete.creditsExhausted.addCredits": "Añadir créditos",
"kilocode:autocomplete.authError.message":
"Kilo Code Autocomplete se ha pausado debido a un error de autenticación. Por favor, inicia sesión de nuevo.",
}
@@ -1,49 +0,0 @@
// fr runtime translations for autocomplete
export const dict = {
"kilocode:autocomplete.statusBar.enabled": "$(kilo-logo) Autocomplete",
"kilocode:autocomplete.statusBar.snoozed": "en pause",
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (désactivé)",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Aucun modèle d'autocomplétion configuré**\n\nPour activer l'autocomplétion, ajoute un profil avec l'un de ces fournisseurs pris en charge : {{providers}}.\n\n[Ouvrir les Paramètres]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Coût total de la session :",
"kilocode:autocomplete.statusBar.tooltip.provider": "Fournisseur:",
"kilocode:autocomplete.statusBar.tooltip.model": "Modèle :",
"kilocode:autocomplete.statusBar.tooltip.profile": "Profil : ",
"kilocode:autocomplete.statusBar.tooltip.defaultProfile": "Par défaut",
"kilocode:autocomplete.statusBar.tooltip.completionSummary":
"{{count}} complétions effectuées entre {{startTime}} et {{endTime}}, pour un coût total de {{cost}}.",
"kilocode:autocomplete.statusBar.tooltip.providerInfo": "Auto-complétions fournies par {{model}} via {{provider}}.",
"kilocode:autocomplete.statusBar.cost.zero": "$0.00",
"kilocode:autocomplete.statusBar.cost.lessThanCent": "<$0.01",
"kilocode:autocomplete.toggleMessage": "Kilo Code Autocomplete {{status}}",
"kilocode:autocomplete.progress.title": "Kilo Code",
"kilocode:autocomplete.progress.analyzing": "Analyse de ton code...",
"kilocode:autocomplete.progress.generating": "Génération des modifications suggérées...",
"kilocode:autocomplete.progress.processing": "Traitement des modifications suggérées...",
"kilocode:autocomplete.progress.showing": "Affichage des modifications suggérées...",
"kilocode:autocomplete.input.title": "Kilo Code : Tâche Rapide",
"kilocode:autocomplete.input.placeholder": "ex., 'refactorise cette fonction pour plus d'efficacité'",
"kilocode:autocomplete.commands.generateSuggestions": "Kilo Code : Générer des Modifications Suggérées",
"kilocode:autocomplete.commands.displaySuggestions": "Afficher les Modifications Suggérées",
"kilocode:autocomplete.commands.cancelSuggestions": "Annuler les Modifications Suggérées",
"kilocode:autocomplete.commands.applyCurrentSuggestion": "Appliquer la Modification Suggérée Actuelle",
"kilocode:autocomplete.commands.applyAllSuggestions": "Appliquer Toutes les Modifications Suggérées",
"kilocode:autocomplete.commands.category": "Kilo Code",
"kilocode:autocomplete.codeAction.title": "Kilo Code : Modifications Suggérées",
"kilocode:autocomplete.chatParticipant.fullName": "Kilo Code Agent",
"kilocode:autocomplete.chatParticipant.name": "Agent",
"kilocode:autocomplete.chatParticipant.description":
"Je peux t'aider avec des tâches rapides et des modifications suggérées.",
"kilocode:autocomplete.incompatibilityExtensionPopup.message":
"Le Kilo Code Autocomplete est bloqué par un conflit avec GitHub Copilot. Pour résoudre cela, tu dois désactiver les suggestions en ligne de Copilot.",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Désactiver Copilot",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Désactiver Autocomplete",
"kilocode:autocomplete.creditsExhausted.message":
"Kilo Code Autocomplete a été mis en pause car ton compte n'a plus de crédits. Ajoute des crédits pour reprendre l'autocomplétion.",
"kilocode:autocomplete.creditsExhausted.addCredits": "Ajouter des crédits",
"kilocode:autocomplete.authError.message":
"Kilo Code Autocomplete a été mis en pause en raison d'une erreur d'authentification. Veuillez te reconnecter.",
}
@@ -1,50 +0,0 @@
// hi runtime translations for autocomplete
export const dict = {
"kilocode:autocomplete.statusBar.enabled": "$(kilo-logo) Autocomplete",
"kilocode:autocomplete.statusBar.snoozed": "रोका गया",
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (अक्षम)",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**कोई ऑटोकम्प्लीट मॉडल कॉन्फ़िगर नहीं किया गया**\n\nऑटोकम्प्लीट सक्षम करने के लिए, इन समर्थित प्रदाताओं में से एक के साथ एक प्रोफ़ाइल जोड़ें: {{providers}}।\n\n[सेटिंग्स खोलें]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "सत्र की कुल लागत:",
"kilocode:autocomplete.statusBar.tooltip.provider": "प्रदाता:",
"kilocode:autocomplete.statusBar.tooltip.model": "मॉडल:",
"kilocode:autocomplete.statusBar.tooltip.profile": "प्रोफ़ाइल: ",
"kilocode:autocomplete.statusBar.tooltip.defaultProfile": "डिफ़ॉल्ट",
"kilocode:autocomplete.statusBar.tooltip.completionSummary":
"{{startTime}} और {{endTime}} के बीच {{count}} पूर्णताएं की गईं, कुल लागत {{cost}}।",
"kilocode:autocomplete.statusBar.tooltip.providerInfo":
"स्वत: पूर्णता {{provider}} के माध्यम से {{model}} द्वारा प्रदान की जाती है।",
"kilocode:autocomplete.statusBar.cost.zero": "$0.00",
"kilocode:autocomplete.statusBar.cost.lessThanCent": "<$0.01",
"kilocode:autocomplete.toggleMessage": "Kilo Code Autocomplete {{status}}",
"kilocode:autocomplete.progress.title": "Kilo Code",
"kilocode:autocomplete.progress.analyzing": "आपके कोड का विश्लेषण कर रहे हैं...",
"kilocode:autocomplete.progress.generating": "सुझाए गए संपादन बना रहे हैं...",
"kilocode:autocomplete.progress.processing": "सुझाए गए संपादन प्रोसेस कर रहे हैं...",
"kilocode:autocomplete.progress.showing": "सुझाए गए संपादन दिखा रहे हैं...",
"kilocode:autocomplete.input.title": "Kilo Code: त्वरित कार्य",
"kilocode:autocomplete.input.placeholder": "जैसे, 'इस फ़ंक्शन को अधिक कुशल बनाने के लिए रिफैक्टर करें'",
"kilocode:autocomplete.commands.generateSuggestions": "Kilo Code: सुझाए गए संपादन जेनरेट करें",
"kilocode:autocomplete.commands.displaySuggestions": "सुझाए गए संपादन दिखाएं",
"kilocode:autocomplete.commands.cancelSuggestions": "सुझाए गए संपादन रद्द करें",
"kilocode:autocomplete.commands.applyCurrentSuggestion": "वर्तमान सुझाए गए संपादन लागू करें",
"kilocode:autocomplete.commands.applyAllSuggestions": "सभी सुझाए गए संपादन लागू करें",
"kilocode:autocomplete.commands.category": "Kilo Code",
"kilocode:autocomplete.codeAction.title": "Kilo Code: सुझाए गए संपादन",
"kilocode:autocomplete.chatParticipant.fullName": "Kilo Code Agent",
"kilocode:autocomplete.chatParticipant.name": "Agent",
"kilocode:autocomplete.chatParticipant.description":
"मैं त्वरित कार्यों और सुझाए गए संपादनों में आपकी सहायता कर सकता हूं।",
"kilocode:autocomplete.incompatibilityExtensionPopup.message":
"Kilo Code Autocomplete को GitHub Copilot के साथ संघर्ष के कारण ब्लॉक किया जा रहा है। इसे ठीक करने के लिए, आपको Copilot के इनलाइन सुझावों को अक्षम करना होगा।",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Copilot अक्षम करें",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Autocomplete अक्षम करें",
"kilocode:autocomplete.creditsExhausted.message":
"Kilo Code Autocomplete रोक दिया गया है क्योंकि आपके खाते में शेष क्रेडिट नहीं है। ऑटोकम्प्लीट फिर से शुरू करने के लिए क्रेडिट जोड़ें।",
"kilocode:autocomplete.creditsExhausted.addCredits": "क्रेडिट जोड़ें",
"kilocode:autocomplete.authError.message":
"प्रमाणीकरण त्रुटि के कारण Kilo Code Autocomplete रोक दिया गया है। कृपया फिर से साइन इन करें।",
}
@@ -1,50 +0,0 @@
// id runtime translations for autocomplete
export const dict = {
"kilocode:autocomplete.statusBar.enabled": "$(kilo-logo) Autocomplete",
"kilocode:autocomplete.statusBar.snoozed": "dijeda",
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (dinonaktifkan)",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Tidak ada model autocomplete yang dikonfigurasi**\n\nUntuk mengaktifkan autocomplete, tambahkan profil dengan salah satu penyedia yang didukung ini: {{providers}}.\n\n[Buka Pengaturan]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Total biaya sesi:",
"kilocode:autocomplete.statusBar.tooltip.provider": "Penyedia:",
"kilocode:autocomplete.statusBar.tooltip.model": "Model:",
"kilocode:autocomplete.statusBar.tooltip.profile": "Profil: ",
"kilocode:autocomplete.statusBar.tooltip.defaultProfile": "Default",
"kilocode:autocomplete.statusBar.tooltip.completionSummary":
"Melakukan {{count}} penyelesaian antara {{startTime}} dan {{endTime}}, dengan total biaya {{cost}}.",
"kilocode:autocomplete.statusBar.tooltip.providerInfo":
"Pelengkapan otomatis disediakan oleh {{model}} melalui {{provider}}.",
"kilocode:autocomplete.statusBar.cost.zero": "$0.00",
"kilocode:autocomplete.statusBar.cost.lessThanCent": "<$0.01",
"kilocode:autocomplete.toggleMessage": "Kilo Code Autocomplete {{status}}",
"kilocode:autocomplete.progress.title": "Kilo Code",
"kilocode:autocomplete.progress.analyzing": "Menganalisis kode Anda...",
"kilocode:autocomplete.progress.generating": "Menghasilkan suntingan yang disarankan...",
"kilocode:autocomplete.progress.processing": "Memproses suntingan yang disarankan...",
"kilocode:autocomplete.progress.showing": "Menampilkan suntingan yang disarankan...",
"kilocode:autocomplete.input.title": "Kilo Code: Tugas Cepat",
"kilocode:autocomplete.input.placeholder": "mis., 'refaktor fungsi ini agar lebih efisien'",
"kilocode:autocomplete.commands.generateSuggestions": "Kilo Code: Hasilkan Suntingan yang Disarankan",
"kilocode:autocomplete.commands.displaySuggestions": "Tampilkan Suntingan yang Disarankan",
"kilocode:autocomplete.commands.cancelSuggestions": "Batalkan Suntingan yang Disarankan",
"kilocode:autocomplete.commands.applyCurrentSuggestion": "Terapkan Suntingan yang Disarankan Saat Ini",
"kilocode:autocomplete.commands.applyAllSuggestions": "Terapkan Semua Suntingan yang Disarankan",
"kilocode:autocomplete.commands.category": "Kilo Code",
"kilocode:autocomplete.codeAction.title": "Kilo Code: Suntingan yang Disarankan",
"kilocode:autocomplete.chatParticipant.fullName": "Kilo Code Agent",
"kilocode:autocomplete.chatParticipant.name": "Agent",
"kilocode:autocomplete.chatParticipant.description":
"Saya dapat membantu Anda dengan tugas cepat dan suntingan yang disarankan.",
"kilocode:autocomplete.incompatibilityExtensionPopup.message":
"Kilo Code Autocomplete diblokir oleh konflik dengan GitHub Copilot. Untuk memperbaiki ini, Anda harus menonaktifkan saran inline Copilot.",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Nonaktifkan Copilot",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Nonaktifkan Autocomplete",
"kilocode:autocomplete.creditsExhausted.message":
"Kilo Code Autocomplete telah dijeda karena akun Anda tidak memiliki sisa kredit. Tambahkan kredit untuk melanjutkan autocomplete.",
"kilocode:autocomplete.creditsExhausted.addCredits": "Tambah Kredit",
"kilocode:autocomplete.authError.message":
"Kilo Code Autocomplete telah dijeda karena kesalahan autentikasi. Silakan masuk kembali.",
}
@@ -1,49 +0,0 @@
// it runtime translations for autocomplete
export const dict = {
"kilocode:autocomplete.statusBar.enabled": "$(kilo-logo) Autocomplete",
"kilocode:autocomplete.statusBar.snoozed": "in pausa",
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (disabilitato)",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Nessun modello di autocompletamento configurato**\n\nPer abilitare l'autocompletamento, aggiungi un profilo con uno di questi provider supportati: {{providers}}.\n\n[Apri Impostazioni]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Costo totale della sessione:",
"kilocode:autocomplete.statusBar.tooltip.provider": "Provider:",
"kilocode:autocomplete.statusBar.tooltip.model": "Modello:",
"kilocode:autocomplete.statusBar.tooltip.profile": "Profilo: ",
"kilocode:autocomplete.statusBar.tooltip.defaultProfile": "Predefinito",
"kilocode:autocomplete.statusBar.tooltip.completionSummary":
"Eseguiti {{count}} completamenti tra {{startTime}} e {{endTime}}, per un costo totale di {{cost}}.",
"kilocode:autocomplete.statusBar.tooltip.providerInfo":
"Autocompletamenti forniti da {{model}} tramite {{provider}}.",
"kilocode:autocomplete.statusBar.cost.zero": "$0.00",
"kilocode:autocomplete.statusBar.cost.lessThanCent": "<$0.01",
"kilocode:autocomplete.toggleMessage": "Kilo Code Autocomplete {{status}}",
"kilocode:autocomplete.progress.title": "Kilo Code",
"kilocode:autocomplete.progress.analyzing": "Analizzando il tuo codice...",
"kilocode:autocomplete.progress.generating": "Generando modifiche suggerite...",
"kilocode:autocomplete.progress.processing": "Elaborando modifiche suggerite...",
"kilocode:autocomplete.progress.showing": "Mostrando modifiche suggerite...",
"kilocode:autocomplete.input.title": "Kilo Code: Attività Rapida",
"kilocode:autocomplete.input.placeholder": "es., 'refactorizza questa funzione per renderla più efficiente'",
"kilocode:autocomplete.commands.generateSuggestions": "Kilo Code: Genera Modifiche Suggerite",
"kilocode:autocomplete.commands.displaySuggestions": "Mostra Modifiche Suggerite",
"kilocode:autocomplete.commands.cancelSuggestions": "Annulla Modifiche Suggerite",
"kilocode:autocomplete.commands.applyCurrentSuggestion": "Applica Modifica Suggerita Corrente",
"kilocode:autocomplete.commands.applyAllSuggestions": "Applica Tutte le Modifiche Suggerite",
"kilocode:autocomplete.commands.category": "Kilo Code",
"kilocode:autocomplete.codeAction.title": "Kilo Code: Modifiche Suggerite",
"kilocode:autocomplete.chatParticipant.fullName": "Kilo Code Agent",
"kilocode:autocomplete.chatParticipant.name": "Agent",
"kilocode:autocomplete.chatParticipant.description": "Posso aiutarti con attività rapide e modifiche suggerite.",
"kilocode:autocomplete.incompatibilityExtensionPopup.message":
"Il Kilo Code Autocomplete è bloccato da un conflitto con GitHub Copilot. Per risolvere questo problema, devi disabilitare i suggerimenti in linea di Copilot.",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Disabilita Copilot",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Disabilita Autocomplete",
"kilocode:autocomplete.creditsExhausted.message":
"Kilo Code Autocomplete è stato messo in pausa perché il tuo account non ha più crediti. Aggiungi crediti per riprendere l'autocompletamento.",
"kilocode:autocomplete.creditsExhausted.addCredits": "Aggiungi crediti",
"kilocode:autocomplete.authError.message":
"Kilo Code Autocomplete è stato messo in pausa a causa di un errore di autenticazione. Effettua nuovamente l'accesso.",
}
@@ -1,49 +0,0 @@
// ja runtime translations for autocomplete
export const dict = {
"kilocode:autocomplete.statusBar.enabled": "$(kilo-logo) Autocomplete",
"kilocode:autocomplete.statusBar.snoozed": "一時停止中",
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete(無効)",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**オートコンプリートモデルが設定されていません**\n\nオートコンプリートを有効にするには、これらのサポートされているプロバイダーのいずれかでプロファイルを追加してください: {{providers}}。\n\n[設定を開く]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "セッション合計コスト:",
"kilocode:autocomplete.statusBar.tooltip.provider": "プロバイダー:",
"kilocode:autocomplete.statusBar.tooltip.model": "モデル:",
"kilocode:autocomplete.statusBar.tooltip.profile": "プロファイル: ",
"kilocode:autocomplete.statusBar.tooltip.defaultProfile": "デフォルト",
"kilocode:autocomplete.statusBar.tooltip.completionSummary":
"{{startTime}}から{{endTime}}の間に{{count}}回の補完を実行し、合計コストは{{cost}}です。",
"kilocode:autocomplete.statusBar.tooltip.providerInfo":
"オートコンプリートは{{provider}}経由で{{model}}によって提供されています。",
"kilocode:autocomplete.statusBar.cost.zero": "$0.00",
"kilocode:autocomplete.statusBar.cost.lessThanCent": "<$0.01",
"kilocode:autocomplete.toggleMessage": "Kilo Code Autocomplete {{status}}",
"kilocode:autocomplete.progress.title": "Kilo Code",
"kilocode:autocomplete.progress.analyzing": "コードを分析中...",
"kilocode:autocomplete.progress.generating": "編集提案を生成中...",
"kilocode:autocomplete.progress.processing": "編集提案を処理中...",
"kilocode:autocomplete.progress.showing": "編集提案を表示中...",
"kilocode:autocomplete.input.title": "Kilo Code: クイックタスク",
"kilocode:autocomplete.input.placeholder": "例:「この関数をより効率的にリファクタリングして」",
"kilocode:autocomplete.commands.generateSuggestions": "Kilo Code: 編集提案を生成",
"kilocode:autocomplete.commands.displaySuggestions": "編集提案を表示",
"kilocode:autocomplete.commands.cancelSuggestions": "編集提案をキャンセル",
"kilocode:autocomplete.commands.applyCurrentSuggestion": "現在の編集提案を適用",
"kilocode:autocomplete.commands.applyAllSuggestions": "すべての編集提案を適用",
"kilocode:autocomplete.commands.category": "Kilo Code",
"kilocode:autocomplete.codeAction.title": "Kilo Code: 編集提案",
"kilocode:autocomplete.chatParticipant.fullName": "Kilo Code Agent",
"kilocode:autocomplete.chatParticipant.name": "Agent",
"kilocode:autocomplete.chatParticipant.description": "クイックタスクと編集提案でお手伝いできます。",
"kilocode:autocomplete.incompatibilityExtensionPopup.message":
"Kilo Code AutocompleteがGitHub Copilotとの競合によってブロックされています。これを修正するには、Copilotのインライン提案を無効にする必要があります。",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Copilotを無効にする",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Autocompleteを無効にする",
"kilocode:autocomplete.creditsExhausted.message":
"アカウントのクレジットが残っていないため、Kilo Code Autocompleteが一時停止されました。オートコンプリートを再開するにはクレジットを追加してください。",
"kilocode:autocomplete.creditsExhausted.addCredits": "クレジットを追加",
"kilocode:autocomplete.authError.message":
"認証エラーのため、Kilo Code Autocompleteが一時停止されました。再度サインインしてください。",
}
@@ -1,48 +0,0 @@
// ko runtime translations for autocomplete
export const dict = {
"kilocode:autocomplete.statusBar.enabled": "$(kilo-logo) Autocomplete",
"kilocode:autocomplete.statusBar.snoozed": "일시 중지됨",
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (비활성화됨)",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**자동완성 모델이 구성되지 않았습니다**\n\n자동완성을 활성화하려면 다음 지원되는 제공업체 중 하나로 프로필을 추가하세요: {{providers}}.\n\n[설정 열기]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "세션 총 비용:",
"kilocode:autocomplete.statusBar.tooltip.provider": "제공업체:",
"kilocode:autocomplete.statusBar.tooltip.model": "모델:",
"kilocode:autocomplete.statusBar.tooltip.profile": "프로필: ",
"kilocode:autocomplete.statusBar.tooltip.defaultProfile": "기본값",
"kilocode:autocomplete.statusBar.tooltip.completionSummary":
"{{startTime}}부터 {{endTime}}까지 {{count}}회 완성을 수행했으며, 총 비용은 {{cost}}입니다.",
"kilocode:autocomplete.statusBar.tooltip.providerInfo": "자동완성은 {{provider}}를 통해 {{model}}에서 제공됩니다.",
"kilocode:autocomplete.statusBar.cost.zero": "$0.00",
"kilocode:autocomplete.statusBar.cost.lessThanCent": "<$0.01",
"kilocode:autocomplete.toggleMessage": "Kilo Code Autocomplete {{status}}",
"kilocode:autocomplete.progress.title": "Kilo Code",
"kilocode:autocomplete.progress.analyzing": "코드 분석 중...",
"kilocode:autocomplete.progress.generating": "제안된 편집 생성 중...",
"kilocode:autocomplete.progress.processing": "제안된 편집 처리 중...",
"kilocode:autocomplete.progress.showing": "제안된 편집 표시 중...",
"kilocode:autocomplete.input.title": "Kilo Code: 빠른 작업",
"kilocode:autocomplete.input.placeholder": "예: '이 함수를 더 효율적으로 리팩토링해줘'",
"kilocode:autocomplete.commands.generateSuggestions": "Kilo Code: 제안된 편집 생성",
"kilocode:autocomplete.commands.displaySuggestions": "제안된 편집 표시",
"kilocode:autocomplete.commands.cancelSuggestions": "제안된 편집 취소",
"kilocode:autocomplete.commands.applyCurrentSuggestion": "현재 제안된 편집 적용",
"kilocode:autocomplete.commands.applyAllSuggestions": "모든 제안된 편집 적용",
"kilocode:autocomplete.commands.category": "Kilo Code",
"kilocode:autocomplete.codeAction.title": "Kilo Code: 제안된 편집",
"kilocode:autocomplete.chatParticipant.fullName": "Kilo Code Agent",
"kilocode:autocomplete.chatParticipant.name": "Agent",
"kilocode:autocomplete.chatParticipant.description": "빠른 작업과 제안된 편집으로 도움을 드릴 수 있습니다.",
"kilocode:autocomplete.incompatibilityExtensionPopup.message":
"Kilo Code Autocomplete가 GitHub Copilot과의 충돌로 인해 차단되고 있습니다. 이를 해결하려면 Copilot의 인라인 제안을 비활성화해야 합니다.",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Copilot 비활성화",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Autocomplete 비활성화",
"kilocode:autocomplete.creditsExhausted.message":
"계정에 남은 크레딧이 없어 Kilo Code Autocomplete가 일시 중지되었습니다. 자동완성을 재개하려면 크레딧을 추가하세요.",
"kilocode:autocomplete.creditsExhausted.addCredits": "크레딧 추가",
"kilocode:autocomplete.authError.message":
"인증 오류로 인해 Kilo Code Autocomplete가 일시 중지되었습니다. 다시 로그인해 주세요.",
}
@@ -1,50 +0,0 @@
// Dutch runtime translations for autocomplete (kilocode:autocomplete.* namespace)
// Source: src/i18n/locales/nl/kilocode.json → "autocomplete" section
export const dict = {
"kilocode:autocomplete.statusBar.enabled": "$(kilo-logo) Autocomplete",
"kilocode:autocomplete.statusBar.snoozed": "gepauzeerd",
"kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete",
"kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (uitgeschakeld)",
"kilocode:autocomplete.statusBar.tooltip.noUsableProvider":
"**Geen autocomplete-model geconfigureerd**\n\nVoeg een profiel toe met een van deze ondersteunde providers om autocomplete in te schakelen: {{providers}}.\n\n[Instellingen openen]({{command}})",
"kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Totale sessiekosten:",
"kilocode:autocomplete.statusBar.tooltip.provider": "Provider:",
"kilocode:autocomplete.statusBar.tooltip.model": "Model:",
"kilocode:autocomplete.statusBar.tooltip.profile": "Profiel: ",
"kilocode:autocomplete.statusBar.tooltip.defaultProfile": "Standaard",
"kilocode:autocomplete.statusBar.tooltip.completionSummary":
"{{count}} aanvullingen uitgevoerd tussen {{startTime}} en {{endTime}}, voor totale kosten van {{cost}}.",
"kilocode:autocomplete.statusBar.tooltip.providerInfo":
"Automatische aanvullingen geleverd door {{model}} via {{provider}}.",
"kilocode:autocomplete.statusBar.cost.zero": "$0.00",
"kilocode:autocomplete.statusBar.cost.lessThanCent": "<$0.01",
"kilocode:autocomplete.toggleMessage": "Kilo Code Autocomplete {{status}}",
"kilocode:autocomplete.progress.title": "Kilo Code",
"kilocode:autocomplete.progress.analyzing": "Je code analyseren...",
"kilocode:autocomplete.progress.generating": "Voorgestelde bewerkingen genereren...",
"kilocode:autocomplete.progress.processing": "Voorgestelde bewerkingen verwerken...",
"kilocode:autocomplete.progress.showing": "Voorgestelde bewerkingen weergeven...",
"kilocode:autocomplete.input.title": "Kilo Code: Snelle taak",
"kilocode:autocomplete.input.placeholder": "bijv. 'herschrijf deze functie om efficiënter te zijn'",
"kilocode:autocomplete.commands.generateSuggestions": "Kilo Code: Voorgestelde bewerkingen genereren",
"kilocode:autocomplete.commands.displaySuggestions": "Voorgestelde bewerkingen weergeven",
"kilocode:autocomplete.commands.cancelSuggestions": "Voorgestelde bewerkingen annuleren",
"kilocode:autocomplete.commands.applyCurrentSuggestion": "Huidige voorgestelde bewerking toepassen",
"kilocode:autocomplete.commands.applyAllSuggestions": "Alle voorgestelde bewerkingen toepassen",
"kilocode:autocomplete.commands.category": "Kilo Code",
"kilocode:autocomplete.codeAction.title": "Kilo Code: Voorgestelde bewerkingen",
"kilocode:autocomplete.chatParticipant.fullName": "Kilo Code Agent",
"kilocode:autocomplete.chatParticipant.name": "Agent",
"kilocode:autocomplete.chatParticipant.description": "Ik kan je helpen met snelle taken en voorgestelde bewerkingen.",
"kilocode:autocomplete.incompatibilityExtensionPopup.message":
"De Kilo Code Autocomplete wordt geblokkeerd door een conflict met GitHub Copilot. Om dit op te lossen, moet je de inline suggesties van Copilot uitschakelen.",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Copilot uitschakelen",
"kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Autocomplete uitschakelen",
"kilocode:autocomplete.creditsExhausted.message":
"Kilo Code Autocomplete is gepauzeerd omdat je account geen tegoed meer heeft. Voeg tegoed toe om autocomplete te hervatten.",
"kilocode:autocomplete.creditsExhausted.addCredits": "Tegoed toevoegen",
"kilocode:autocomplete.authError.message":
"Kilo Code Autocomplete is gepauzeerd vanwege een authenticatiefout. Meld je opnieuw aan.",
}
@@ -1,13 +0,0 @@
// ar package.nls translations for autocomplete
export const dict = {
"autocomplete.input.title": "اضغط 'Enter' للتأكيد أو 'Escape' للإلغاء",
"autocomplete.input.placeholder": "صف ما تريد فعله...",
"autocomplete.commands.generateSuggestions": "Kilo Code: إنشاء التعديلات المقترحة",
"autocomplete.commands.displaySuggestions": "عرض التعديلات المقترحة",
"autocomplete.commands.cancelSuggestions": "إلغاء التعديلات المقترحة",
"autocomplete.commands.applyCurrentSuggestion": "تطبيق التعديل المقترح الحالي",
"autocomplete.commands.applyAllSuggestions": "تطبيق جميع التعديلات المقترحة",
"autocomplete.commands.goToNextSuggestion": "انتقل إلى الاقتراح التالي",
"autocomplete.commands.goToPreviousSuggestion": "انتقل إلى الاقتراح السابق",
}
@@ -1,13 +0,0 @@
// ca package.nls translations for autocomplete
export const dict = {
"autocomplete.input.title": "Premeu 'Enter' per confirmar o 'Escape' per cancel·lar",
"autocomplete.input.placeholder": "Descriviu què voleu fer...",
"autocomplete.commands.generateSuggestions": "Kilo Code: Generar Edicions Suggerides",
"autocomplete.commands.displaySuggestions": "Mostrar Edicions Suggerides",
"autocomplete.commands.cancelSuggestions": "Cancel·lar Edicions Suggerides",
"autocomplete.commands.applyCurrentSuggestion": "Aplicar Edició Suggerida Actual",
"autocomplete.commands.applyAllSuggestions": "Aplicar Totes les Edicions Suggerides",
"autocomplete.commands.goToNextSuggestion": "Anar al Suggeriment Següent",
"autocomplete.commands.goToPreviousSuggestion": "Anar al Suggeriment Anterior",
}
@@ -1,13 +0,0 @@
// cs package.nls translations for autocomplete
export const dict = {
"autocomplete.input.title": "Stiskněte 'Enter' pro potvrzení nebo 'Escape' pro zrušení",
"autocomplete.input.placeholder": "Popište, co chcete udělat...",
"autocomplete.commands.generateSuggestions": "Kilo Code: Generovat Navrhované Úpravy",
"autocomplete.commands.displaySuggestions": "Zobrazit Navrhované Úpravy",
"autocomplete.commands.cancelSuggestions": "Zrušit Navrhované Úpravy",
"autocomplete.commands.applyCurrentSuggestion": "Použít Aktuální Navrženou Úpravu",
"autocomplete.commands.applyAllSuggestions": "Použít Všechny Navrhované Úpravy",
"autocomplete.commands.goToNextSuggestion": "Přejít na Další Návrh",
"autocomplete.commands.goToPreviousSuggestion": "Přejít na Předchozí Návrh",
}
@@ -1,13 +0,0 @@
// de package.nls translations for autocomplete
export const dict = {
"autocomplete.input.title": "Kilo Code Geisterschreiber",
"autocomplete.input.placeholder": "Beschreiben Sie, was Sie programmieren möchten...",
"autocomplete.commands.generateSuggestions": "Kilo Code: Vorgeschlagene Bearbeitungen Generieren",
"autocomplete.commands.displaySuggestions": "Vorgeschlagene Bearbeitungen Anzeigen",
"autocomplete.commands.cancelSuggestions": "Vorgeschlagene Bearbeitungen Abbrechen",
"autocomplete.commands.applyCurrentSuggestion": "Aktuelle Vorgeschlagene Bearbeitung Anwenden",
"autocomplete.commands.applyAllSuggestions": "Alle Vorgeschlagenen Bearbeitungen Anwenden",
"autocomplete.commands.goToNextSuggestion": "Zur Nächsten Vorgeschlagenen Bearbeitung",
"autocomplete.commands.goToPreviousSuggestion": "Zur Vorherigen Vorgeschlagenen Bearbeitung",
}
@@ -1,14 +0,0 @@
// English package.nls translations for autocomplete (used in package.json command titles/descriptions)
// Source: src/package.nls.json → "autocomplete.*" keys
export const dict = {
"autocomplete.input.title": "Press 'Enter' to confirm or 'Escape' to cancel",
"autocomplete.input.placeholder": "Describe what you want to do...",
"autocomplete.commands.generateSuggestions": "Generate Suggested Edits",
"autocomplete.commands.displaySuggestions": "Display Suggested Edits",
"autocomplete.commands.cancelSuggestions": "Cancel Suggested Edits",
"autocomplete.commands.applyCurrentSuggestion": "Apply Current Suggested Edit",
"autocomplete.commands.applyAllSuggestions": "Apply All Suggested Edits",
"autocomplete.commands.goToNextSuggestion": "Go To Next Suggestion",
"autocomplete.commands.goToPreviousSuggestion": "Go To Previous Suggestion",
}

Some files were not shown because too many files have changed in this diff Show More