Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/node/copilot/fileEditTracker.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { decodeHex, encodeHex, VSBuffer } from '../../../../base/common/buffer.js';
7
import { basename } from '../../../../base/common/path.js';
8
import { URI } from '../../../../base/common/uri.js';
9
import { IFileService } from '../../../files/common/files.js';
10
import { ILogService } from '../../../log/common/log.js';
11
import { IDiffComputeService } from '../../common/diffComputeService.js';
12
import { ISessionDatabase } from '../../common/sessionDataService.js';
13
import { FileEditKind, ToolResultContentType, type ToolResultFileEditContent } from '../../common/state/sessionState.js';
14
15
const SESSION_DB_SCHEME = 'session-db';
16
17
/**
18
* Builds a `session-db:` URI that references a file-edit content blob
19
* stored in the session database. Parsed by {@link parseSessionDbUri}.
20
*/
21
export function buildSessionDbUri(sessionUri: string, toolCallId: string, filePath: string, part: 'before' | 'after'): string {
22
return URI.from({
23
scheme: SESSION_DB_SCHEME,
24
authority: encodeHex(VSBuffer.fromString(sessionUri)).toString(),
25
path: `/${encodeURIComponent(toolCallId)}/${encodeHex(VSBuffer.fromString(filePath))}/${part}/${basename(filePath)}`,
26
}).toString();
27
}
28
29
/** Parsed fields from a `session-db:` content URI. */
30
export interface ISessionDbUriFields {
31
sessionUri: string;
32
toolCallId: string;
33
filePath: string;
34
part: 'before' | 'after';
35
}
36
37
/**
38
* Parses a `session-db:` URI produced by {@link buildSessionDbUri}.
39
* Returns `undefined` if the URI is not a valid `session-db:` URI.
40
*/
41
export function parseSessionDbUri(raw: string): ISessionDbUriFields | undefined {
42
const parsed = URI.parse(raw);
43
if (parsed.scheme !== SESSION_DB_SCHEME) {
44
return undefined;
45
}
46
const [, toolCallId, filePath, part] = parsed.path.split('/');
47
if (!toolCallId || !filePath || (part !== 'before' && part !== 'after')) {
48
return undefined;
49
}
50
try {
51
return {
52
sessionUri: decodeHex(parsed.authority).toString(),
53
toolCallId: decodeURIComponent(toolCallId),
54
filePath: decodeHex(filePath).toString(),
55
part
56
};
57
} catch {
58
return undefined;
59
}
60
}
61
62
/**
63
* Tracks file edits made by tools in a session by snapshotting file content
64
* before and after each edit tool invocation, persisting snapshots into the
65
* session database.
66
*/
67
export class FileEditTracker {
68
69
/**
70
* Pending edits keyed by file path. The `onPreToolUse` hook stores
71
* entries here; `completeEdit` pops them when the tool finishes.
72
*/
73
private readonly _pendingEdits = new Map<string, { beforeContent: VSBuffer; snapshotDone: Promise<void> }>();
74
75
/**
76
* Completed edits keyed by file path. The `onPostToolUse` hook stores
77
* entries here; `takeCompletedEdit` retrieves them from the
78
* `onToolComplete` handler and persists to the database.
79
*/
80
private readonly _completedEdits = new Map<string, { beforeContent: VSBuffer; afterContent: VSBuffer }>();
81
82
constructor(
83
private readonly _sessionUri: string,
84
private readonly _db: ISessionDatabase,
85
@IFileService private readonly _fileService: IFileService,
86
@ILogService private readonly _logService: ILogService,
87
@IDiffComputeService private readonly _diffComputeService: IDiffComputeService,
88
) { }
89
90
/**
91
* Call from the `onPreToolUse` hook before an edit tool runs.
92
* Reads the file's current content into memory as the "before" state.
93
* The hook blocks the SDK until this returns, ensuring the snapshot
94
* captures pre-edit content.
95
*
96
* @param filePath - Absolute path of the file being edited.
97
*/
98
async trackEditStart(filePath: string): Promise<void> {
99
const snapshotDone = this._readFile(filePath);
100
const entry = { beforeContent: VSBuffer.fromString(''), snapshotDone: snapshotDone.then(buf => { entry.beforeContent = buf; }) };
101
this._pendingEdits.set(filePath, entry);
102
await entry.snapshotDone;
103
}
104
105
/**
106
* Call from the `onPostToolUse` hook after an edit tool finishes.
107
* Reads the file content again as the "after" state and stores the
108
* result for later retrieval via {@link takeCompletedEdit}.
109
*
110
* @param filePath - Absolute path of the file that was edited.
111
*/
112
async completeEdit(filePath: string): Promise<void> {
113
const pending = this._pendingEdits.get(filePath);
114
if (!pending) {
115
return;
116
}
117
this._pendingEdits.delete(filePath);
118
await pending.snapshotDone;
119
120
const afterContent = await this._readFile(filePath);
121
122
this._completedEdits.set(filePath, {
123
beforeContent: pending.beforeContent,
124
afterContent,
125
});
126
}
127
128
/**
129
* Retrieves and removes a completed edit for the given file path,
130
* persists it to the session database with computed diff counts,
131
* and returns the result as an {@link ToolResultFileEditContent}
132
* for inclusion in the tool result.
133
*
134
* @param toolCallId - The tool call that produced this edit.
135
* @param filePath - Absolute path of the edited file.
136
*/
137
async takeCompletedEdit(turnId: string, toolCallId: string, filePath: string): Promise<ToolResultFileEditContent | undefined> {
138
const edit = this._completedEdits.get(filePath);
139
if (!edit) {
140
return undefined;
141
}
142
this._completedEdits.delete(filePath);
143
144
const beforeBytes = edit.beforeContent.buffer;
145
const afterBytes = edit.afterContent.buffer;
146
const beforeText = edit.beforeContent.toString();
147
const afterText = edit.afterContent.toString();
148
149
let addedLines: number | undefined;
150
let removedLines: number | undefined;
151
try {
152
const counts = await this._diffComputeService.computeDiffCounts(beforeText, afterText);
153
addedLines = counts.added;
154
removedLines = counts.removed;
155
} catch (err) {
156
this._logService.warn(`[FileEditTracker] Failed to compute diff counts: ${filePath}`, err);
157
}
158
159
try {
160
await this._db.storeFileEdit({
161
turnId,
162
toolCallId,
163
filePath,
164
kind: FileEditKind.Edit,
165
beforeContent: beforeBytes,
166
afterContent: afterBytes,
167
addedLines,
168
removedLines,
169
});
170
} catch (err) {
171
this._logService.warn(`[FileEditTracker] Failed to persist file edit to database: ${filePath}`, err);
172
}
173
174
return {
175
type: ToolResultContentType.FileEdit,
176
before: {
177
uri: URI.file(filePath).toString(),
178
content: { uri: buildSessionDbUri(this._sessionUri, toolCallId, filePath, 'before') },
179
},
180
after: {
181
uri: URI.file(filePath).toString(),
182
content: { uri: buildSessionDbUri(this._sessionUri, toolCallId, filePath, 'after') },
183
},
184
diff: addedLines !== undefined ? { added: addedLines, removed: removedLines } : undefined,
185
};
186
}
187
188
private async _readFile(filePath: string): Promise<VSBuffer> {
189
try {
190
const content = await this._fileService.readFile(URI.file(filePath));
191
return content.value;
192
} catch (err) {
193
this._logService.trace(`[FileEditTracker] Could not read file for snapshot: ${filePath}`, err);
194
return VSBuffer.fromString('');
195
}
196
}
197
}
198
199