How Continue's Edit Aggregation Works for Multi-File Changes
Continue's edit aggregation system clusters rapid keystrokes into meaningful diffs using the EditAggregator class, ensuring that edits across multiple files are isolated into separate clusters before being combined into a unified context for the LLM.
Continue is an open-source AI code assistant (continuedev/continue) that tracks user edits in real-time to provide context-aware suggestions. The edit aggregation mechanism transforms streams of individual keystrokes into logical change clusters, handling multi-file scenarios by finalizing clusters when users switch between files.
The Core Architecture of Edit Aggregation
The EditAggregator class in core/nextEdit/context/aggregateEdits.ts serves as the central engine for transforming raw text edits into structured diffs.
File-Specific State Management
The aggregator maintains isolated state for each file using a Map<string, FileState> structure stored in EditAggregator.fileStates. Each FileState tracks:
- Current file content
- A queue of pending edit tasks (
processingQueue) - Active clusters of edits for that specific file
This isolation ensures that edit history from one file never contaminates another during the clustering process.
Cluster Formation and Thresholds
A cluster represents a group of consecutive small edits forming a single logical change. Defined by the ClusterState interface, each cluster records:
- The before snapshot of the file (
beforeState) - Line ranges affected by the edits
- Timestamps and cursor positions
- The complete list of individual edits
The EditClusterConfig interface defines configurable thresholds that determine cluster boundaries:
deltaT: Time gap (in seconds) before forcing a new clusterdeltaL: Maximum line distance between edits to remain in the same clustermaxEdits: Maximum number of edits allowed per clustermaxDuration: Total time limit for a cluster's lifespan
Multi-File Edit Aggregation Pipeline
When processing edits across multiple files, the aggregator follows a strict pipeline to maintain file isolation while enabling unified context generation.
File Switch Detection
The critical mechanism for multi-file support resides in the processEdit method (lines 16-22 of aggregateEdits.ts). When an edit arrives for a different file than the previous one, the aggregator immediately finalizes all pending clusters for the previous file:
// core/nextEdit/context/aggregateEdits.ts
if (this.lastProcessedFilePath && this.lastProcessedFilePath !== filePath) {
await this.finalizeClustersForFile(this.lastProcessedFilePath);
}
this.lastProcessedFilePath = filePath;
This ensures that file A's edits never mix with file B's edits within the same cluster, preserving the integrity of per-file diffs.
Edit Processing Workflow
- Edit Arrival: Each small edit (
RangeInFileWithNextEditInfo) enters viaprocessEdit - Queue Management: Edits wrap into tasks and enter the file-specific
processingQueue, processed in batches of up to five (_processQueue) - Cluster Assignment:
_processEditInternalcallsfindSuitableClusterto determine if the edit fits within an existing cluster based on proximity, time gaps, and edit counts - Cluster Expansion: If suitable, the cluster updates its line range and timestamps; otherwise, a new cluster initializes with the file's pre-edit content
- Structural Boundaries: Edits inserting newlines or spanning multiple lines trigger immediate finalization of overlapping clusters to prevent mixing unrelated logical changes
Diff Generation and Finalization
When finalizeCluster executes, it generates a unified diff using createDiff and passes it to EditAggregator.onComparisonFinalized. The system filters out whitespace-only diffs or excessively large changes. The NextEditProvider sets this callback to receive finalized diffs for prompt construction.
Integrating with NextEditProvider
The NextEditProvider class (core/nextEdit/NextEditProvider.ts) consumes aggregated diffs to build LLM prompts. It merges finalized diffs with in-progress editing context:
// core/nextEdit/NextEditProvider.ts (excerpt)
const inProgressDiff = EditAggregator.getInstance().getInProgressDiff(
helper.filepath,
);
if (inProgressDiff) {
combinedDiffContext.push(inProgressDiff);
}
This integration occurs in NextEditProvider._generatePrompts (lines 96-107), where the provider combines:
- All finalized diffs from previous edits (
this.diffContext) - The current in-progress diff from the active file (
inProgressDiff)
The resulting combinedDiffContext presents the LLM with a complete, multi-file view of recent changes while maintaining clear boundaries between files.
Practical Implementation Examples
Configuring the EditAggregator
Instantiate the singleton with custom thresholds for specific aggregation behavior:
import { EditAggregator } from "core/nextEdit/context/aggregateEdits";
const aggregator = EditAggregator.getInstance({
deltaT: 1.5, // seconds before forcing new cluster
deltaL: 3, // line distance threshold
maxEdits: 200, // maximum edits per cluster
});
const edit = {
filepath: "/project/src/util.ts",
fileContents: "const newValue = 42;",
fileContentsBefore: "const oldValue = 0;",
range: { start: { line: 10, character: 0 }, end: { line: 10, character: 18 } },
editText: "const newValue = 42;",
beforeCursorPos: { line: 10, character: 0 },
afterCursorPos: { line: 10, character: 20 },
};
await aggregator.processEdit(edit);
Retrieving In-Progress Diffs
Access current editing context before finalization:
const currentDiff = EditAggregator.getInstance().getInProgressDiff(
"/project/src/main.ts"
);
Manual Finalization
Force immediate finalization of all pending clusters, useful during application shutdown or context switches:
await EditAggregator.getInstance().finalizeAllClusters();
Key Source Files
| File | Role |
|---|---|
core/nextEdit/context/aggregateEdits.ts |
Core aggregation engine implementing EditAggregator, clusters, and diff creation |
core/nextEdit/NextEditProvider.ts |
Consumes aggregated diffs and builds LLM prompts via _generatePrompts |
core/nextEdit/context/processSmallEdit.ts |
Helper recording finalized diffs into the provider context |
core/edit/searchAndReplace/* |
Validation utilities feeding edit objects to the aggregator |
core/edit/streamDiffLines.ts |
Unified diff generation used by createDiff |
Summary
- EditAggregator (
aggregateEdits.ts) clusters rapid keystrokes into logical diffs using configurable time, line, and count thresholds. - Multi-file isolation occurs through
finalizeClustersForFile, which flushes pending clusters when users switch files. - Cluster state tracks before/after snapshots, line ranges, and edit metadata via the
FileStatemap structure. - NextEditProvider merges finalized diffs with in-progress edits using
getInProgressDiffbefore sending context to the LLM. - Structural edits (newlines, multi-line changes) trigger immediate cluster finalization to preserve logical boundaries.
Frequently Asked Questions
How does Continue prevent edit clusters from mixing between different files?
When EditAggregator.processEdit receives an edit for a new file path, it detects the change via lastProcessedFilePath comparison and immediately calls finalizeClustersForFile for the previous file. This forces all pending clusters from the previous file to generate diffs before processing begins on the new file, ensuring complete isolation between file contexts.
What triggers a cluster to finalize into a diff?
Clusters finalize when they exceed thresholds defined in EditClusterConfig: time gaps exceeding deltaT, line distances beyond deltaL, edit counts surpassing maxEdits, or total duration exceeding maxDuration. Additionally, structural edits spanning multiple lines or inserting newlines trigger immediate finalization of overlapping clusters to prevent mixing unrelated changes.
Can I access edits before the cluster finalizes?
Yes. The EditAggregator.getInProgressDiff method returns the current diff for an active file before finalization. NextEditProvider uses this in _generatePrompts to combine in-progress changes with already-finalized diffs, ensuring the LLM receives real-time context including unsaved modifications.
Where does the actual diff generation happen?
The finalizeCluster method inside EditAggregator calls createDiff to generate unified diffs. This utility leverages logic from core/edit/streamDiffLines.ts to produce the diff format, which is then passed to the onComparisonFinalized callback configured by NextEditProvider.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →