Browse Source

Merge pull request #3506 from Kilo-Org/beatlevic/ghost-context-provider

Ghost context provider
Coen Stevens 3 months ago
parent
commit
b18e3f3e2d

+ 7 - 1
src/services/continuedev/core/vscode-test-harness/src/autocomplete/RecentlyVisitedRangesService.ts

@@ -14,6 +14,7 @@ export class RecentlyVisitedRangesService {
 	private maxRecentFiles = 3
 	private maxSnippetsPerFile = 3
 	private isEnabled = true
+	private disposable: vscode.Disposable | undefined
 
 	constructor(private readonly ide: IDE) {
 		this.cache = new LRUCache<string, Array<AutocompleteCodeSnippet & { timestamp: number }>>({
@@ -32,7 +33,7 @@ export class RecentlyVisitedRangesService {
 			this.numSurroundingLines = recentlyVisitedRangesNumSurroundingLines
 		}
 
-		vscode.window.onDidChangeTextEditorSelection(this.cacheCurrentSelectionContext)
+		this.disposable = vscode.window.onDidChangeTextEditorSelection(this.cacheCurrentSelectionContext)
 	}
 
 	private cacheCurrentSelectionContext = async (event: vscode.TextEditorSelectionChangeEvent) => {
@@ -105,4 +106,9 @@ export class RecentlyVisitedRangesService {
 			.sort((a, b) => b.timestamp - a.timestamp)
 			.map(({ timestamp: _timestamp, ...snippet }) => snippet)
 	}
+
+	public dispose(): void {
+		this.disposable?.dispose()
+		this.cache.clear()
+	}
 }

+ 13 - 2
src/services/continuedev/core/vscode-test-harness/src/autocomplete/recentlyEdited.ts

@@ -21,9 +21,11 @@ export class RecentlyEditedTracker {
 
 	private recentlyEditedDocuments: VsCodeRecentlyEditedDocument[] = []
 	private static maxRecentlyEditedDocuments = 10
+	private disposable: vscode.Disposable | undefined
+	private cleanupInterval: NodeJS.Timeout | undefined
 
 	constructor(private ide: IDE) {
-		vscode.workspace.onDidChangeTextDocument((event) => {
+		this.disposable = vscode.workspace.onDidChangeTextDocument((event) => {
 			event.contentChanges.forEach((change) => {
 				const editedRange = {
 					uri: event.document.uri,
@@ -39,7 +41,7 @@ export class RecentlyEditedTracker {
 			this.insertDocument(event.document.uri)
 		})
 
-		setInterval(() => {
+		this.cleanupInterval = setInterval(() => {
 			this.removeOldEntries()
 		}, 1000 * 15)
 	}
@@ -128,4 +130,13 @@ export class RecentlyEditedTracker {
 			}
 		})
 	}
+
+	public dispose(): void {
+		this.disposable?.dispose()
+		if (this.cleanupInterval) {
+			clearInterval(this.cleanupInterval)
+		}
+		this.recentlyEditedRanges = []
+		this.recentlyEditedDocuments = []
+	}
 }

+ 6 - 0
src/services/ghost/GhostServiceManager.ts

@@ -6,6 +6,7 @@ import { GhostModel } from "./GhostModel"
 import { GhostStatusBar } from "./GhostStatusBar"
 import { GhostCodeActionProvider } from "./GhostCodeActionProvider"
 import { GhostInlineCompletionProvider } from "./classic-auto-complete/GhostInlineCompletionProvider"
+import { GhostContextProvider } from "./classic-auto-complete/GhostContextProvider"
 //import { NewAutocompleteProvider } from "./new-auto-complete/NewAutocompleteProvider"
 import { GhostServiceSettings, TelemetryEventName } from "@roo-code/types"
 import { ContextProxy } from "../../core/config/ContextProxy"
@@ -22,6 +23,7 @@ export class GhostServiceManager {
 	private context: vscode.ExtensionContext
 	private settings: GhostServiceSettings | null = null
 	private ghostContext: GhostContext
+	private ghostContextProvider: GhostContextProvider
 
 	private taskId: string | null = null
 	private isProcessing: boolean = false
@@ -47,6 +49,7 @@ export class GhostServiceManager {
 		this.documentStore = new GhostDocumentStore()
 		this.model = new GhostModel()
 		this.ghostContext = new GhostContext(this.documentStore)
+		this.ghostContextProvider = new GhostContextProvider(context, this.model)
 
 		// Register the providers
 		this.codeActionProvider = new GhostCodeActionProvider()
@@ -55,6 +58,7 @@ export class GhostServiceManager {
 			this.updateCostTracking.bind(this),
 			this.ghostContext,
 			() => this.settings,
+			this.ghostContextProvider,
 		)
 
 		// Register document event handlers
@@ -432,6 +436,8 @@ export class GhostServiceManager {
 			this.inlineCompletionProviderDisposable = null
 		}
 
+		// Dispose inline completion provider resources
+		this.inlineCompletionProvider.dispose()
 		// Dispose new autocomplete provider if it exists
 		//if (this.newAutocompleteProvider) {
 		//	this.newAutocompleteProvider.dispose()

+ 0 - 153
src/services/ghost/__tests__/GhostRecentOperations.spec.ts

@@ -1,153 +0,0 @@
-import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"
-import * as vscode from "vscode"
-import { GhostContext } from "../GhostContext"
-import { GhostDocumentStore } from "../GhostDocumentStore"
-import { HoleFiller } from "../classic-auto-complete/HoleFiller"
-import { GhostSuggestionContext, contextToAutocompleteInput } from "../types"
-import { MockTextDocument } from "../../mocking/MockTextDocument"
-
-// Mock vscode
-vi.mock("vscode", () => ({
-	Uri: {
-		parse: (uriString: string) => ({
-			toString: () => uriString,
-			fsPath: uriString.replace("file://", ""),
-			scheme: "file",
-			path: uriString.replace("file://", ""),
-		}),
-	},
-	workspace: {
-		asRelativePath: vi.fn().mockImplementation((uri) => {
-			if (typeof uri === "string") {
-				return uri.replace("file:///", "")
-			}
-			return uri.toString().replace("file:///", "")
-		}),
-		textDocuments: [], // Mock textDocuments as an empty array
-	},
-	window: {
-		activeTextEditor: null,
-	},
-	languages: {
-		getDiagnostics: vi.fn().mockReturnValue([]), // Mock getDiagnostics to return empty array
-	},
-	Position: class {
-		constructor(
-			public line: number,
-			public character: number,
-		) {}
-	},
-	Range: class {
-		constructor(
-			public start: any,
-			public end: any,
-		) {}
-	},
-	DiagnosticSeverity: {
-		Error: 0,
-		Warning: 1,
-		Information: 2,
-		Hint: 3,
-	},
-}))
-
-// Mock diff - using importOriginal as recommended in the error message
-vi.mock("diff", async (importOriginal) => {
-	// Create a mock module with the functions we need
-	return {
-		createPatch: vi.fn().mockImplementation((filePath, oldContent, newContent) => {
-			return `--- a/${filePath}\n+++ b/${filePath}\n@@ -1,1 +1,1 @@\n-${oldContent}\n+${newContent}`
-		}),
-		structuredPatch: vi.fn().mockImplementation((oldFileName, newFileName, oldContent, newContent) => {
-			return {
-				hunks: [
-					{
-						oldStart: 1,
-						oldLines: 1,
-						newStart: 1,
-						newLines: 1,
-						lines: [`-${oldContent}`, `+${newContent}`],
-					},
-				],
-			}
-		}),
-		parsePatch: vi.fn().mockReturnValue([]),
-	}
-})
-
-describe("GhostRecentOperations", () => {
-	let documentStore: GhostDocumentStore
-	let context: GhostContext
-	let holeFiller: HoleFiller
-	let mockDocument: MockTextDocument
-
-	beforeEach(() => {
-		documentStore = new GhostDocumentStore()
-		context = new GhostContext(documentStore)
-		holeFiller = new HoleFiller()
-
-		// Create a mock document
-		const uri = vscode.Uri.parse("file:///test-file.ts")
-		mockDocument = new MockTextDocument(uri, "test-content")
-	})
-
-	afterEach(() => {
-		vi.clearAllMocks()
-	})
-
-	it("should include recent operations in the prompt when available", async () => {
-		// Store initial document version
-		await documentStore.storeDocument({ document: mockDocument, bypassDebounce: true })
-
-		// Update document content and store again
-		mockDocument.updateContent("test-content-updated")
-		await documentStore.storeDocument({ document: mockDocument, bypassDebounce: true })
-
-		// Create a suggestion context
-		const suggestionContext: GhostSuggestionContext = {
-			document: mockDocument,
-		}
-
-		// Generate context with recent operations
-		const enrichedContext = await context.generate(suggestionContext)
-
-		// Verify that recent operations were added to the context
-		expect(enrichedContext.recentOperations).toBeDefined()
-		expect(enrichedContext.recentOperations?.length).toBeGreaterThan(0)
-
-		const autocompleteInput = contextToAutocompleteInput(enrichedContext)
-
-		// Generate prompt
-		const prefix = enrichedContext.document.getText()
-		const suffix = ""
-		const languageId = enrichedContext.document.languageId
-		const { userPrompt } = holeFiller.getPrompts(autocompleteInput, prefix, suffix, languageId)
-
-		// Verify that the prompt includes the recent operations section
-		// The strategy system uses "<RECENT_EDITS>" XML format
-		expect(userPrompt).toContain("<RECENT_EDITS>")
-	})
-
-	it("should not include recent operations in the prompt when not available", async () => {
-		// Create a suggestion context without storing document history
-		const suggestionContext: GhostSuggestionContext = {
-			document: mockDocument,
-		}
-
-		// Generate context
-		const enrichedContext = await context.generate(suggestionContext)
-
-		const autocompleteInput = contextToAutocompleteInput(enrichedContext)
-
-		// Generate prompt
-		const prefix = enrichedContext.document.getText()
-		const suffix = ""
-		const languageId = enrichedContext.document.languageId
-		const { userPrompt } = holeFiller.getPrompts(autocompleteInput, prefix, suffix, languageId)
-
-		// Verify that the prompt does not include recent operations section
-		// The current document content will still be in the prompt, so we should only check
-		// that the "**Recent Changes (Diff):**" section is not present
-		expect(userPrompt.includes("**Recent Changes (Diff):**")).toBe(false)
-	})
-})

+ 78 - 0
src/services/ghost/classic-auto-complete/GhostContextProvider.ts

@@ -0,0 +1,78 @@
+import * as vscode from "vscode"
+import { ContextRetrievalService } from "../../continuedev/core/autocomplete/context/ContextRetrievalService"
+import { VsCodeIde } from "../../continuedev/core/vscode-test-harness/src/VSCodeIde"
+import { AutocompleteInput } from "../types"
+import { HelperVars } from "../../continuedev/core/autocomplete/util/HelperVars"
+import { getAllSnippetsWithoutRace } from "../../continuedev/core/autocomplete/snippets/getAllSnippets"
+import { getDefinitionsFromLsp } from "../../continuedev/core/vscode-test-harness/src/autocomplete/lsp"
+import { DEFAULT_AUTOCOMPLETE_OPTS } from "../../continuedev/core/util/parameters"
+import { getSnippets } from "../../continuedev/core/autocomplete/templating/filtering"
+import { formatSnippets } from "../../continuedev/core/autocomplete/templating/formatting"
+import { GhostModel } from "../GhostModel"
+
+export class GhostContextProvider {
+	private contextService: ContextRetrievalService
+	private ide: VsCodeIde
+	private model: GhostModel
+
+	constructor(context: vscode.ExtensionContext, model: GhostModel) {
+		this.ide = new VsCodeIde(context)
+		this.contextService = new ContextRetrievalService(this.ide)
+		this.model = model
+	}
+
+	/**
+	 * Get the IDE instance for use by tracking services
+	 */
+	public getIde(): VsCodeIde {
+		return this.ide
+	}
+
+	/**
+	 * Get context snippets for the current autocomplete request
+	 * Returns comment-based formatted context that can be added to prompts
+	 */
+	async getFormattedContext(autocompleteInput: AutocompleteInput, filepath: string): Promise<string> {
+		// Convert filepath to URI if it's not already one
+		const filepathUri = filepath.startsWith("file://") ? filepath : vscode.Uri.file(filepath).toString()
+
+		// Initialize import definitions cache
+		// this looks like a race, but the contextService only prefetches data here; it's not a mode switch.
+		// This odd-looking API seems to be an optimization that's used in continue but not (currently) in our codebase,
+		// continue preloads the tree-sitter parse on text editor tab switch to reduce autocomplete latency.
+		await this.contextService.initializeForFile(filepathUri)
+
+		// Create helper with URI filepath
+		const helperInput = {
+			...autocompleteInput,
+			filepath: filepathUri,
+		}
+
+		const modelName = this.model.getModelName() ?? "codestral"
+		const helper = await HelperVars.create(helperInput as any, DEFAULT_AUTOCOMPLETE_OPTS, modelName, this.ide)
+
+		const snippetPayload = await getAllSnippetsWithoutRace({
+			helper,
+			ide: this.ide,
+			getDefinitionsFromLsp,
+			contextRetrievalService: this.contextService,
+		})
+
+		const filteredSnippets = getSnippets(helper, snippetPayload)
+
+		// Convert all snippet filepaths to URIs
+		const snippetsWithUris = filteredSnippets.map((snippet: any) => ({
+			...snippet,
+			filepath: snippet.filepath?.startsWith("file://")
+				? snippet.filepath
+				: vscode.Uri.file(snippet.filepath).toString(),
+		}))
+
+		const workspaceDirs = await this.ide.getWorkspaceDirs()
+		const formattedContext = formatSnippets(helper, snippetsWithUris, workspaceDirs)
+
+		console.log("[GhostContextProvider] - formattedContext:", formattedContext)
+
+		return formattedContext
+	}
+}

+ 37 - 6
src/services/ghost/classic-auto-complete/GhostInlineCompletionProvider.ts

@@ -1,9 +1,12 @@
 import * as vscode from "vscode"
 import { extractPrefixSuffix, GhostSuggestionContext, contextToAutocompleteInput } from "../types"
+import { GhostContextProvider } from "./GhostContextProvider"
 import { parseGhostResponse, HoleFiller, FillInAtCursorSuggestion } from "./HoleFiller"
 import { GhostModel } from "../GhostModel"
 import { GhostContext } from "../GhostContext"
 import { ApiStreamChunk } from "../../../api/transform/stream"
+import { RecentlyVisitedRangesService } from "../../continuedev/core/vscode-test-harness/src/autocomplete/RecentlyVisitedRangesService"
+import { RecentlyEditedTracker } from "../../continuedev/core/vscode-test-harness/src/autocomplete/recentlyEdited"
 import type { GhostServiceSettings } from "@roo-code/types"
 import { refuseUselessSuggestion } from "./uselessSuggestionFilter"
 
@@ -76,18 +79,30 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte
 	private costTrackingCallback: CostTrackingCallback
 	private ghostContext: GhostContext
 	private getSettings: () => GhostServiceSettings | null
+	private recentlyVisitedRangesService: RecentlyVisitedRangesService
+	private recentlyEditedTracker: RecentlyEditedTracker
 
 	constructor(
 		model: GhostModel,
 		costTrackingCallback: CostTrackingCallback,
 		ghostContext: GhostContext,
 		getSettings: () => GhostServiceSettings | null,
+		contextProvider?: GhostContextProvider,
 	) {
 		this.model = model
 		this.costTrackingCallback = costTrackingCallback
 		this.ghostContext = ghostContext
 		this.getSettings = getSettings
-		this.holeFiller = new HoleFiller()
+		this.holeFiller = new HoleFiller(contextProvider)
+
+		// Get IDE from context provider if available
+		const ide = contextProvider?.getIde()
+		if (ide) {
+			this.recentlyVisitedRangesService = new RecentlyVisitedRangesService(ide)
+			this.recentlyEditedTracker = new RecentlyEditedTracker(ide)
+		} else {
+			throw new Error("GhostContextProvider with IDE is required for tracking services")
+		}
 	}
 
 	public updateSuggestions(fillInAtCursor: FillInAtCursorSuggestion): void {
@@ -120,7 +135,16 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte
 	public async getFromLLM(context: GhostSuggestionContext, model: GhostModel): Promise<LLMRetrievalResult> {
 		this.isRequestCancelled = false
 
-		const autocompleteInput = contextToAutocompleteInput(context)
+		const recentlyVisitedRanges = this.recentlyVisitedRangesService.getSnippets()
+		const recentlyEditedRanges = await this.recentlyEditedTracker.getRecentlyEditedRanges()
+
+		const enrichedContext: GhostSuggestionContext = {
+			...context,
+			recentlyVisitedRanges,
+			recentlyEditedRanges,
+		}
+
+		const autocompleteInput = contextToAutocompleteInput(enrichedContext)
 
 		const position = context.range?.start ?? context.document.positionAt(0)
 		const { prefix, suffix } = extractPrefixSuffix(context.document, position)
@@ -140,7 +164,12 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte
 			}
 		}
 
-		const { systemPrompt, userPrompt } = this.holeFiller.getPrompts(autocompleteInput, prefix, suffix, languageId)
+		const { systemPrompt, userPrompt } = await this.holeFiller.getPrompts(
+			autocompleteInput,
+			prefix,
+			suffix,
+			languageId,
+		)
 
 		if (this.isRequestCancelled) {
 			return {
@@ -203,13 +232,15 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte
 		}
 	}
 
-	/**
-	 * Cancel any ongoing LLM request
-	 */
 	public cancelRequest(): void {
 		this.isRequestCancelled = true
 	}
 
+	public dispose(): void {
+		this.recentlyVisitedRangesService.dispose()
+		this.recentlyEditedTracker.dispose()
+	}
+
 	public async provideInlineCompletionItems(
 		document: vscode.TextDocument,
 		position: vscode.Position,

+ 35 - 16
src/services/ghost/classic-auto-complete/HoleFiller.ts

@@ -1,4 +1,5 @@
 import { AutocompleteInput } from "../types"
+import { GhostContextProvider } from "./GhostContextProvider"
 
 export interface FillInAtCursorSuggestion {
 	text: string
@@ -9,8 +10,15 @@ export interface FillInAtCursorSuggestion {
 export function getBaseSystemInstructions(): string {
 	return `You are a HOLE FILLER. You are provided with a file containing holes, formatted as '{{FILL_HERE}}'. Your TASK is to complete with a string to replace this hole with, inside a <COMPLETION/> XML tag, including context-aware indentation, if needed. All completions MUST be truthful, accurate, well-written and correct.
 
-## Context Tags
-<LANGUAGE>: file language | <RECENT_EDITS>: recent changes | <QUERY>: code with {{FILL_HERE}}
+## CRITICAL RULES
+- NEVER repeat or duplicate content that appears immediately before {{FILL_HERE}}
+- If {{FILL_HERE}} is at the end of a comment line, start your completion with a newline and new code
+- Maintain proper indentation matching the surrounding code
+
+## Context Format
+<LANGUAGE>: file language
+<QUERY>: contains commented reference code (// Path: file.ts) followed by code with {{FILL_HERE}}
+Comments provide context from related files, recent edits, imports, etc.
 
 ## EXAMPLE QUERY:
 
@@ -123,18 +131,21 @@ export function parseGhostResponse(fullResponse: string, prefix: string, suffix:
 }
 
 export class HoleFiller {
-	getPrompts(
+	constructor(private contextProvider?: GhostContextProvider) {}
+
+	async getPrompts(
 		autocompleteInput: AutocompleteInput,
 		prefix: string,
 		suffix: string,
 		languageId: string,
-	): {
+	): Promise<{
 		systemPrompt: string
 		userPrompt: string
-	} {
+	}> {
+		const userPrompt = await this.getUserPrompt(autocompleteInput, prefix, suffix, languageId)
 		return {
 			systemPrompt: this.getSystemInstructions(),
-			userPrompt: this.getUserPrompt(autocompleteInput, prefix, suffix, languageId),
+			userPrompt,
 		}
 	}
 
@@ -149,22 +160,30 @@ Provide a subtle, non-intrusive completion after a typing pause.
 	}
 
 	/**
-	 * Build minimal prompt for auto-trigger
+	 * Build minimal prompt for auto-trigger with optional context
 	 */
-	getUserPrompt(autocompleteInput: AutocompleteInput, prefix: string, suffix: string, languageId: string): string {
+	async getUserPrompt(
+		autocompleteInput: AutocompleteInput,
+		prefix: string,
+		suffix: string,
+		languageId: string,
+	): Promise<string> {
 		let prompt = `<LANGUAGE>${languageId}</LANGUAGE>\n\n`
 
-		if (autocompleteInput.recentlyEditedRanges && autocompleteInput.recentlyEditedRanges.length > 0) {
-			prompt += "<RECENT_EDITS>\n"
-			autocompleteInput.recentlyEditedRanges.forEach((range, index) => {
-				const description = `Edited ${range.filepath} at line ${range.range.start.line}`
-				prompt += `${index + 1}. ${description}\n`
-			})
-			prompt += "</RECENT_EDITS>\n\n"
+		let formattedContext = ""
+		if (this.contextProvider && autocompleteInput.filepath) {
+			try {
+				formattedContext = await this.contextProvider.getFormattedContext(
+					autocompleteInput,
+					autocompleteInput.filepath,
+				)
+			} catch (error) {
+				console.warn("Failed to get formatted context:", error)
+			}
 		}
 
 		prompt += `<QUERY>
-${prefix}{{FILL_HERE}}${suffix}
+${formattedContext}${prefix}{{FILL_HERE}}${suffix}
 </QUERY>
 
 TASK: Fill the {{FILL_HERE}} hole. Answer only with the CORRECT completion, and NOTHING ELSE. Do it now.

+ 200 - 0
src/services/ghost/classic-auto-complete/__tests__/GhostContextProvider.test.ts

@@ -0,0 +1,200 @@
+import { describe, it, expect, beforeEach, vi } from "vitest"
+import { GhostContextProvider } from "../GhostContextProvider"
+import { AutocompleteInput } from "../../types"
+import { AutocompleteSnippetType } from "../../../continuedev/core/autocomplete/snippets/types"
+import { GhostModel } from "../../GhostModel"
+import * as vscode from "vscode"
+import crypto from "crypto"
+
+vi.mock("vscode", () => ({
+	Uri: {
+		parse: (uriString: string) => ({
+			toString: () => uriString,
+			fsPath: uriString.replace("file://", ""),
+		}),
+		file: (path: string) => ({
+			toString: () => `file://${path}`,
+			fsPath: path,
+		}),
+	},
+	workspace: {
+		textDocuments: [],
+		workspaceFolders: [],
+	},
+	window: {
+		activeTextEditor: null,
+	},
+}))
+
+vi.mock("../../../continuedev/core/autocomplete/context/ContextRetrievalService", () => ({
+	ContextRetrievalService: vi.fn().mockImplementation(() => ({
+		initializeForFile: vi.fn().mockResolvedValue(undefined),
+	})),
+}))
+
+vi.mock("../../../continuedev/core/vscode-test-harness/src/VSCodeIde", () => ({
+	VsCodeIde: vi.fn().mockImplementation(() => ({
+		getWorkspaceDirs: vi.fn().mockResolvedValue(["file:///workspace"]),
+	})),
+}))
+
+vi.mock("../../../continuedev/core/autocomplete/util/HelperVars", () => ({
+	HelperVars: {
+		create: vi.fn().mockResolvedValue({
+			filepath: "file:///test.ts",
+			lang: { name: "typescript", singleLineComment: "//" },
+		}),
+	},
+}))
+
+vi.mock("../../../continuedev/core/autocomplete/snippets/getAllSnippets", () => ({
+	getAllSnippetsWithoutRace: vi.fn().mockResolvedValue({
+		recentlyOpenedFileSnippets: [],
+		importDefinitionSnippets: [],
+		rootPathSnippets: [],
+		recentlyEditedRangeSnippets: [],
+		recentlyVisitedRangesSnippets: [],
+		diffSnippets: [],
+		clipboardSnippets: [],
+		ideSnippets: [],
+		staticSnippet: [],
+	}),
+}))
+
+vi.mock("../../../continuedev/core/autocomplete/templating/filtering", () => ({
+	getSnippets: vi
+		.fn()
+		.mockImplementation((_helper, payload) => [
+			...payload.recentlyOpenedFileSnippets,
+			...payload.importDefinitionSnippets,
+		]),
+}))
+
+vi.mock("../../../continuedev/core/autocomplete/templating/formatting", () => ({
+	formatSnippets: vi.fn().mockImplementation((helper, snippets) => {
+		if (snippets.length === 0) return ""
+		const comment = helper.lang.singleLineComment
+		return snippets.map((s: any) => `${comment} Path: ${s.filepath}\n${s.content}`).join("\n")
+	}),
+}))
+
+function createAutocompleteInput(filepath: string = "/test.ts"): AutocompleteInput {
+	return {
+		isUntitledFile: false,
+		completionId: crypto.randomUUID(),
+		filepath,
+		pos: { line: 0, character: 0 },
+		recentlyVisitedRanges: [],
+		recentlyEditedRanges: [],
+	}
+}
+
+describe("GhostContextProvider", () => {
+	let contextProvider: GhostContextProvider
+	let mockContext: vscode.ExtensionContext
+	let mockModel: GhostModel
+
+	beforeEach(() => {
+		vi.clearAllMocks()
+		mockContext = {
+			subscriptions: [],
+			globalState: {
+				get: vi.fn(),
+				update: vi.fn(),
+			},
+		} as any
+
+		mockModel = {
+			getModelName: vi.fn().mockReturnValue("codestral"),
+		} as any
+
+		contextProvider = new GhostContextProvider(mockContext, mockModel)
+	})
+
+	describe("getFormattedContext", () => {
+		it("should return empty string when no snippets available", async () => {
+			const input = createAutocompleteInput("/test.ts")
+			const formatted = await contextProvider.getFormattedContext(input, "/test.ts")
+
+			expect(formatted).toBe("")
+		})
+
+		it("should return formatted context when snippets are available", async () => {
+			const { getAllSnippetsWithoutRace } = await import(
+				"../../../continuedev/core/autocomplete/snippets/getAllSnippets"
+			)
+
+			;(getAllSnippetsWithoutRace as any).mockResolvedValueOnce({
+				recentlyOpenedFileSnippets: [
+					{
+						filepath: "/recent.ts",
+						content: "const recent = 1;",
+						type: AutocompleteSnippetType.Code,
+					},
+				],
+				importDefinitionSnippets: [],
+				rootPathSnippets: [],
+				recentlyEditedRangeSnippets: [],
+				recentlyVisitedRangesSnippets: [],
+				diffSnippets: [],
+				clipboardSnippets: [],
+				ideSnippets: [],
+				staticSnippet: [],
+			})
+
+			const input = createAutocompleteInput("/test.ts")
+			const formatted = await contextProvider.getFormattedContext(input, "/test.ts")
+
+			const expected = "// Path: file:///recent.ts\nconst recent = 1;"
+			expect(formatted).toBe(expected)
+		})
+
+		it("should format multiple snippets correctly", async () => {
+			const { getAllSnippetsWithoutRace } = await import(
+				"../../../continuedev/core/autocomplete/snippets/getAllSnippets"
+			)
+
+			;(getAllSnippetsWithoutRace as any).mockResolvedValueOnce({
+				recentlyOpenedFileSnippets: [
+					{
+						filepath: "/file1.ts",
+						content: "const first = 1;",
+						type: AutocompleteSnippetType.Code,
+					},
+				],
+				importDefinitionSnippets: [
+					{
+						filepath: "/file2.ts",
+						content: "const second = 2;",
+						type: AutocompleteSnippetType.Code,
+					},
+				],
+				rootPathSnippets: [],
+				recentlyEditedRangeSnippets: [],
+				recentlyVisitedRangesSnippets: [],
+				diffSnippets: [],
+				clipboardSnippets: [],
+				ideSnippets: [],
+				staticSnippet: [],
+			})
+
+			const input = createAutocompleteInput("/test.ts")
+			const formatted = await contextProvider.getFormattedContext(input, "/test.ts")
+
+			const expected = "// Path: file:///file1.ts\nconst first = 1;\n// Path: file:///file2.ts\nconst second = 2;"
+			expect(formatted).toBe(expected)
+		})
+
+		it("should propagate errors from getAllSnippetsWithoutRace", async () => {
+			const { getAllSnippetsWithoutRace } = await import(
+				"../../../continuedev/core/autocomplete/snippets/getAllSnippets"
+			)
+
+			;(getAllSnippetsWithoutRace as any).mockRejectedValueOnce(new Error("Test error"))
+
+			const input = createAutocompleteInput("/test.ts")
+
+			await expect(contextProvider.getFormattedContext(input, "/test.ts")).rejects.toThrow("Test error")
+		})
+	})
+})

+ 25 - 1
src/services/ghost/classic-auto-complete/__tests__/GhostInlineCompletionProvider.test.ts

@@ -9,7 +9,7 @@ import { MockTextDocument } from "../../../mocking/MockTextDocument"
 import { GhostModel } from "../../GhostModel"
 import { GhostContext } from "../../GhostContext"
 
-// Mock vscode InlineCompletionTriggerKind enum
+// Mock vscode InlineCompletionTriggerKind enum and event listeners
 vi.mock("vscode", async () => {
 	const actual = await vi.importActual<typeof vscode>("vscode")
 	return {
@@ -18,6 +18,14 @@ vi.mock("vscode", async () => {
 			Invoke: 0,
 			Automatic: 1,
 		},
+		window: {
+			...actual.window,
+			onDidChangeTextEditorSelection: vi.fn(() => ({ dispose: vi.fn() })),
+		},
+		workspace: {
+			...actual.workspace,
+			onDidChangeTextDocument: vi.fn(() => ({ dispose: vi.fn() })),
+		},
 	}
 })
 
@@ -288,6 +296,7 @@ describe("GhostInlineCompletionProvider", () => {
 	let mockCostTrackingCallback: CostTrackingCallback
 	let mockGhostContext: GhostContext
 	let mockSettings: { enableAutoTrigger: boolean } | null
+	let mockContextProvider: any
 
 	beforeEach(() => {
 		mockDocument = new MockTextDocument(vscode.Uri.file("/test.ts"), "const x = 1\nconst y = 2")
@@ -299,6 +308,20 @@ describe("GhostInlineCompletionProvider", () => {
 		mockToken = {} as vscode.CancellationToken
 		mockSettings = { enableAutoTrigger: true }
 
+		// Create mock IDE for tracking services
+		const mockIde = {
+			getWorkspaceDirs: vi.fn().mockResolvedValue([]),
+			getOpenFiles: vi.fn().mockResolvedValue([]),
+			readFile: vi.fn().mockResolvedValue(""),
+			// Add other methods as needed by RecentlyVisitedRangesService and RecentlyEditedTracker
+		}
+
+		// Create mock context provider with IDE
+		mockContextProvider = {
+			getIde: vi.fn().mockReturnValue(mockIde),
+			getFormattedContext: vi.fn().mockResolvedValue(""),
+		}
+
 		// Create mock dependencies
 		mockModel = {
 			generateResponse: vi.fn().mockResolvedValue({
@@ -323,6 +346,7 @@ describe("GhostInlineCompletionProvider", () => {
 			mockCostTrackingCallback,
 			mockGhostContext,
 			() => mockSettings,
+			mockContextProvider,
 		)
 	})
 

+ 42 - 69
src/services/ghost/classic-auto-complete/__tests__/HoleFiller.test.ts

@@ -26,8 +26,8 @@ describe("HoleFiller", () => {
 	})
 
 	describe("getPrompts", () => {
-		it("should generate prompts with QUERY/FILL_HERE format", () => {
-			const { systemPrompt, userPrompt } = holeFiller.getPrompts(
+		it("should generate prompts with QUERY/FILL_HERE format", async () => {
+			const { systemPrompt, userPrompt } = await holeFiller.getPrompts(
 				createAutocompleteInput("/test.ts", 0, 13),
 				"const x = 1;\n",
 				"",
@@ -36,86 +36,59 @@ describe("HoleFiller", () => {
 
 			// Verify system prompt contains auto-trigger keywords
 			expect(systemPrompt).toContain("Auto-Completion")
-			expect(systemPrompt).toContain("non-intrusive")
+			expect(systemPrompt).toContain("Context Format")
 
-			// Verify user prompt uses QUERY/FILL_HERE format
-			expect(userPrompt).toContain("<QUERY>")
-			expect(userPrompt).toContain("{{FILL_HERE}}")
-			expect(userPrompt).toContain("</QUERY>")
-			expect(userPrompt).toContain("COMPLETION")
-		})
+			const expected = `<LANGUAGE>typescript</LANGUAGE>
 
-		it("should document context tags in system prompt", () => {
-			const { systemPrompt } = holeFiller.getPrompts(
-				createAutocompleteInput("/test.ts", 0, 13),
-				"const x = 1;\n",
-				"",
-				"typescript",
-			)
+<QUERY>
+const x = 1;
+{{FILL_HERE}}
+</QUERY>
 
-			// Verify system prompt documents the XML tags
-			expect(systemPrompt).toContain("Context Tags")
-			expect(systemPrompt).toContain("<LANGUAGE>")
-			expect(systemPrompt).toContain("<RECENT_EDITS>")
-			expect(systemPrompt).toContain("<QUERY>")
-		})
-
-		it("should include language ID in prompt with XML tags", () => {
-			const { userPrompt } = holeFiller.getPrompts(
-				createAutocompleteInput("/test.ts", 0, 13),
-				"const x = 1;\n",
-				"",
-				"typescript",
-			)
+TASK: Fill the {{FILL_HERE}} hole. Answer only with the CORRECT completion, and NOTHING ELSE. Do it now.
+Return the COMPLETION tags`
 
-			expect(userPrompt).toContain("<LANGUAGE>typescript</LANGUAGE>")
+			expect(userPrompt).toBe(expected)
 		})
 
-		it("should include recently edited ranges in prompt with XML tags", () => {
-			const input = createAutocompleteInput("/test.ts", 5, 0)
-			input.recentlyEditedRanges = [
-				{
-					filepath: "/test.ts",
-					range: { start: { line: 2, character: 0 }, end: { line: 3, character: 0 } },
-					timestamp: Date.now(),
-					lines: ["function sum(a, b) {"],
-					symbols: new Set(["sum"]),
+		it("should include comment-wrapped context when provider is set", async () => {
+			const mockContextProvider = {
+				getFormattedContext: async () => {
+					// Simulate comment-wrapped format
+					return `// Path: utils.ts
+// export function sum(a: number, b: number) {
+//   return a + b
+// }
+// Path: app.ts
+`
 				},
-			]
+			} as any
 
-			const { userPrompt } = holeFiller.getPrompts(input, "const x = 1;\n", "", "typescript")
-
-			expect(userPrompt).toContain("<RECENT_EDITS>")
-			expect(userPrompt).toContain("</RECENT_EDITS>")
-			expect(userPrompt).toContain("Edited /test.ts at line 2")
-		})
-
-		it("should handle empty recently edited ranges", () => {
-			const { userPrompt } = holeFiller.getPrompts(
-				createAutocompleteInput("/test.ts", 0, 13),
-				"const x = 1;\n",
-				"",
+			const holeFillerWithContext = new HoleFiller(mockContextProvider)
+			const { userPrompt } = await holeFillerWithContext.getPrompts(
+				createAutocompleteInput("/app.ts", 5, 0),
+				"function calculate() {\n  ",
+				"\n}",
 				"typescript",
 			)
 
-			expect(userPrompt).not.toContain("<RECENT_EDITS>")
-			expect(userPrompt).toContain("<LANGUAGE>typescript</LANGUAGE>")
-		})
+			const expected = `<LANGUAGE>typescript</LANGUAGE>
 
-		it("should handle comments in code", () => {
-			const { systemPrompt, userPrompt } = holeFiller.getPrompts(
-				createAutocompleteInput("/test.ts", 1, 0),
-				"// TODO: implement sum function\n",
-				"",
-				"typescript",
-			)
+<QUERY>
+// Path: utils.ts
+// export function sum(a: number, b: number) {
+//   return a + b
+// }
+// Path: app.ts
+function calculate() {
+  {{FILL_HERE}}
+}
+</QUERY>
 
-			// Should use same prompt format
-			expect(systemPrompt).toContain("Auto-Completion")
-			expect(userPrompt).toContain("<QUERY>")
-			expect(userPrompt).toContain("{{FILL_HERE}}")
-			expect(userPrompt).toContain("</QUERY>")
-			expect(userPrompt).toContain("COMPLETION")
+TASK: Fill the {{FILL_HERE}} hole. Answer only with the CORRECT completion, and NOTHING ELSE. Do it now.
+Return the COMPLETION tags`
+
+			expect(userPrompt).toBe(expected)
 		})
 	})
 

+ 17 - 9
src/services/ghost/types.ts

@@ -1,4 +1,6 @@
 import * as vscode from "vscode"
+import type { AutocompleteCodeSnippet } from "../continuedev/core/autocomplete/snippets/types"
+import type { RecentlyEditedRange as ContinuedevRecentlyEditedRange } from "../continuedev/core/autocomplete/util/types"
 
 /**
  * Represents the type of user action performed on a document
@@ -43,6 +45,8 @@ export interface GhostSuggestionContext {
 	userInput?: string
 	recentOperations?: UserAction[] // Stores meaningful user actions instead of raw diff
 	diagnostics?: vscode.Diagnostic[] // Document diagnostics (errors, warnings, etc.)
+	recentlyVisitedRanges?: AutocompleteCodeSnippet[] // Recently visited code snippets for context
+	recentlyEditedRanges?: RecentlyEditedRange[] // Recently edited ranges for context
 }
 
 // ============================================================================
@@ -113,12 +117,9 @@ export interface RecentlyEditedRange extends RangeInFile {
 
 /**
  * Code snippet for autocomplete context
- * Duplicated from continuedev/core to avoid coupling
+ * Re-exported from continuedev/core for compatibility
  */
-export interface AutocompleteCodeSnippet extends RangeInFile {
-	content: string
-	score?: number
-}
+export type { AutocompleteCodeSnippet }
 
 /**
  * Input for autocomplete request (CompletionProvider-compatible)
@@ -224,8 +225,12 @@ export function contextToAutocompleteInput(context: GhostSuggestionContext): Aut
 	const position = context.range?.start ?? context.document.positionAt(0)
 	const { prefix, suffix } = extractPrefixSuffix(context.document, position)
 
-	// Convert recent operations to recently edited ranges
-	const recentlyEditedRanges: RecentlyEditedRange[] =
+	// Get recently visited and edited ranges from context, with empty arrays as fallback
+	const recentlyVisitedRanges = context.recentlyVisitedRanges ?? []
+	const recentlyEditedRanges = context.recentlyEditedRanges ?? []
+
+	// Merge recently edited ranges from context operations with tracked ranges
+	const contextEditedRanges: RecentlyEditedRange[] =
 		context.recentOperations?.map((op) => {
 			const range: Range = op.lineRange
 				? {
@@ -246,13 +251,16 @@ export function contextToAutocompleteInput(context: GhostSuggestionContext): Aut
 			}
 		}) ?? []
 
+	// Combine tracked recently edited ranges with context operations
+	const allRecentlyEditedRanges = [...recentlyEditedRanges, ...contextEditedRanges]
+
 	return {
 		isUntitledFile: context.document.isUntitled,
 		completionId: crypto.randomUUID(),
 		filepath: context.document.uri.fsPath,
 		pos: vscodePositionToPosition(position),
-		recentlyVisitedRanges: [], // Not tracked in current Ghost implementation
-		recentlyEditedRanges,
+		recentlyVisitedRanges,
+		recentlyEditedRanges: allRecentlyEditedRanges,
 		manuallyPassFileContents: undefined,
 		manuallyPassPrefix: prefix,
 	}

+ 6 - 1
src/test-llm-autocompletion/strategy-tester.ts

@@ -125,7 +125,12 @@ export class StrategyTester {
 			recentlyEditedRanges: [],
 		}
 
-		const { systemPrompt, userPrompt } = this.holeFiller.getPrompts(autocompleteInput, prefix, suffix, languageId)
+		const { systemPrompt, userPrompt } = await this.holeFiller.getPrompts(
+			autocompleteInput,
+			prefix,
+			suffix,
+			languageId,
+		)
 
 		const response = await this.llmClient.sendPrompt(systemPrompt, userPrompt)