Skip to content

Commit cb63666

Browse files
authored
Background - agent session changes caching (#3337)
* Initial implementation * Second iteration * Pull request feedback * Fix opening the changes from the session * Disable the Apply action in the empty window * Hide Apply action in empty window
1 parent bdaba62 commit cb63666

File tree

4 files changed

+108
-50
lines changed

4 files changed

+108
-50
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4403,7 +4403,7 @@
44034403
"chat/input/editing/sessionToolbar": [
44044404
{
44054405
"command": "github.copilot.chat.applyCopilotCLIAgentSessionChanges.apply",
4406-
"when": "chatSessionType == copilotcli",
4406+
"when": "chatSessionType == copilotcli && workbenchState != empty",
44074407
"group": "navigation@0"
44084408
},
44094409
{
@@ -4973,7 +4973,7 @@
49734973
"multiDiffEditor/content": [
49744974
{
49754975
"command": "github.copilot.chat.applyCopilotCLIAgentSessionChanges",
4976-
"when": "resourceScheme == copilotcli-worktree-changes"
4976+
"when": "resourceScheme == copilotcli-worktree-changes && workbenchState != empty"
49774977
}
49784978
],
49794979
"chat/chatSessions": [
@@ -4999,7 +4999,7 @@
49994999
},
50005000
{
50015001
"command": "github.copilot.chat.applyCopilotCLIAgentSessionChanges",
5002-
"when": "chatSessionType == copilotcli",
5002+
"when": "chatSessionType == copilotcli && workbenchState != empty",
50035003
"group": "3_apply@0"
50045004
},
50055005
{

src/extension/chatSessions/common/chatSessionWorktreeService.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ import type * as vscode from 'vscode';
77
import { RepoContext } from '../../../platform/git/common/gitService';
88
import { createServiceIdentifier } from '../../../util/common/services';
99

10+
export interface ChatSessionWorktreeFile {
11+
readonly filePath: string;
12+
readonly originalFilePath: string | undefined;
13+
readonly modifiedFilePath: string | undefined;
14+
readonly statistics: {
15+
readonly additions: number;
16+
readonly deletions: number;
17+
};
18+
}
19+
1020
export interface ChatSessionWorktreeData {
1121
readonly data: string;
1222
readonly version: number;
@@ -18,6 +28,7 @@ interface ChatSessionWorktreePropertiesV1 {
1828
readonly branchName: string;
1929
readonly repositoryPath: string;
2030
readonly worktreePath: string;
31+
readonly changes?: readonly ChatSessionWorktreeFile[] | undefined;
2132
}
2233

2334
export type ChatSessionWorktreeProperties = ChatSessionWorktreePropertiesV1;
@@ -36,7 +47,7 @@ export interface IChatSessionWorktreeService {
3647
getWorktreePath(sessionId: string): vscode.Uri | undefined;
3748

3849
applyWorktreeChanges(sessionId: string): Promise<void>;
39-
getWorktreeChanges(sessionId: string): Promise<vscode.ChatSessionChangedFile2[] | undefined>;
50+
getWorktreeChanges(sessionId: string): Promise<readonly ChatSessionWorktreeFile[] | undefined>;
4051

4152
handleRequestCompleted(sessionId: string): Promise<void>;
4253
}

src/extension/chatSessions/vscode-node/chatSessionWorktreeServiceImpl.ts

Lines changed: 71 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,19 @@ import { CancellationToken } from 'vscode-languageserver-protocol';
99
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
1010
import { IGitCommitMessageService } from '../../../platform/git/common/gitCommitMessageService';
1111
import { IGitService, RepoContext } from '../../../platform/git/common/gitService';
12-
import { toGitUri } from '../../../platform/git/common/utils';
1312
import { ILogService } from '../../../platform/log/common/logService';
1413
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
1514
import { Disposable } from '../../../util/vs/base/common/lifecycle';
1615
import * as path from '../../../util/vs/base/common/path';
1716
import { basename, isEqual } from '../../../util/vs/base/common/resources';
18-
import { ChatSessionWorktreeData, ChatSessionWorktreeProperties, IChatSessionWorktreeService } from '../common/chatSessionWorktreeService';
17+
import { ChatSessionWorktreeData, ChatSessionWorktreeFile, ChatSessionWorktreeProperties, IChatSessionWorktreeService } from '../common/chatSessionWorktreeService';
1918

2019
const CHAT_SESSION_WORKTREE_MEMENTO_KEY = 'github.copilot.cli.sessionWorktrees';
2120

2221
export class ChatSessionWorktreeService extends Disposable implements IChatSessionWorktreeService {
2322
declare _serviceBrand: undefined;
2423

2524
private _sessionWorktrees: Map<string, string | ChatSessionWorktreeProperties> = new Map();
26-
private _sessionWorktreeChanges: Map<string, vscode.ChatSessionChangedFile2[] | undefined> = new Map();
2725

2826
constructor(
2927
@IGitCommitMessageService private readonly gitCommitMessageService: IGitCommitMessageService,
@@ -165,8 +163,13 @@ export class ChatSessionWorktreeService extends Disposable implements IChatSessi
165163
untracked: true
166164
});
167165

168-
// Clear worktree changes cache
169-
this._sessionWorktreeChanges.delete(sessionId);
166+
// Delete worktree changes cache
167+
if (worktreeProperties) {
168+
this.setWorktreeProperties(sessionId, {
169+
...worktreeProperties,
170+
changes: undefined
171+
});
172+
}
170173

171174
return;
172175
}
@@ -211,70 +214,82 @@ export class ChatSessionWorktreeService extends Disposable implements IChatSessi
211214
});
212215
}
213216

214-
// Clear worktree changes cache
215-
this._sessionWorktreeChanges.delete(sessionId);
217+
// Delete worktree changes cache
218+
this.setWorktreeProperties(sessionId, {
219+
...worktreeProperties,
220+
changes: undefined
221+
});
216222
}
217223

218-
async getWorktreeChanges(sessionId: string): Promise<vscode.ChatSessionChangedFile2[] | undefined> {
219-
if (this._sessionWorktreeChanges.has(sessionId)) {
220-
return this._sessionWorktreeChanges.get(sessionId);
224+
async getWorktreeChanges(sessionId: string): Promise<readonly ChatSessionWorktreeFile[] | undefined> {
225+
// Get worktree properties
226+
const worktreeProperties = this.getWorktreeProperties(sessionId);
227+
if (!worktreeProperties) {
228+
return undefined;
221229
}
222230

223-
// Check whether the session has an associated worktree
224-
const worktreePath = this.getWorktreePath(sessionId);
225-
if (!worktreePath) {
226-
return undefined;
231+
// Return cached changes
232+
if (worktreeProperties.changes) {
233+
return worktreeProperties.changes;
227234
}
228235

236+
const worktreePath = vscode.Uri.file(worktreeProperties.worktreePath);
237+
229238
// Ensure the initial repository discovery is completed and the repository
230239
// states are initialized in the vscode.git extension. This is needed as these
231240
// will be the repositories that we use to compute the worktree changes. We do
232241
// not have to open each worktree individually since the changes are committed
233242
// so we can get them from the main repository or discovered worktree.
234243
await this.gitService.initialize();
235244

245+
// TODO@lszomoru: Remove this change to support welcome view
236246
// Check whether the worktree belongs to any of the discovered repositories
237247
const repository = this.gitService.repositories
238248
.find(r => r.worktrees.some(w => isEqual(vscode.Uri.file(w.path), worktreePath)));
239249

240250
if (!repository) {
241-
this._sessionWorktreeChanges.set(sessionId, undefined);
242251
return undefined;
243252
}
244253

245-
// Get worktree properties
246-
const worktreeProperties = this.getWorktreeProperties(sessionId);
247-
248-
if (worktreeProperties === undefined || worktreeProperties.autoCommit === false) {
254+
if (worktreeProperties.autoCommit === false) {
249255
// These changes are staged in the worktree but not yet committed. Since the
250256
// changes are not committed, we need to get them from the worktree repository
251257
// state. To do that we need to open the worktree repository. The source control
252258
// provider will not be shown in the Source Control view since it is being hidden.
253259
const worktreeRepository = await this.gitService.getRepository(worktreePath);
254260

255261
if (!worktreeRepository?.changes) {
256-
this._sessionWorktreeChanges.set(sessionId, []);
262+
this.setWorktreeProperties(sessionId, {
263+
...worktreeProperties,
264+
changes: []
265+
});
266+
257267
return [];
258268
}
259269

260-
const changes: vscode.ChatSessionChangedFile2[] = [];
270+
const changes: ChatSessionWorktreeFile[] = [];
261271
for (const change of [...worktreeRepository.changes.indexChanges, ...worktreeRepository.changes.workingTree]) {
262272
try {
263273
const fileStats = await this.gitService.diffIndexWithHEADShortStats(change.uri);
264-
changes.push(new vscode.ChatSessionChangedFile2(
265-
change.uri,
266-
change.status !== 1 /* INDEX_ADDED */
267-
? change.originalUri
274+
changes.push({
275+
filePath: change.uri.fsPath,
276+
originalFilePath: change.status !== 1 /* INDEX_ADDED */
277+
? change.originalUri?.fsPath
268278
: undefined,
269-
change.status !== 2 /* INDEX_DELETED */
270-
? change.uri
279+
modifiedFilePath: change.status !== 2 /* INDEX_DELETED */
280+
? change.uri.fsPath
271281
: undefined,
272-
fileStats?.insertions ?? 0,
273-
fileStats?.deletions ?? 0));
282+
statistics: {
283+
additions: fileStats?.insertions ?? 0,
284+
deletions: fileStats?.deletions ?? 0
285+
}
286+
} satisfies ChatSessionWorktreeFile);
274287
} catch (error) { }
275288
}
276289

277-
this._sessionWorktreeChanges.set(sessionId, changes);
290+
this.setWorktreeProperties(sessionId, {
291+
...worktreeProperties, changes
292+
});
278293
return changes;
279294
}
280295

@@ -287,24 +302,32 @@ export class ChatSessionWorktreeService extends Disposable implements IChatSessi
287302
worktreeProperties.branchName);
288303

289304
if (!diff) {
290-
this._sessionWorktreeChanges.set(sessionId, []);
305+
this.setWorktreeProperties(sessionId, {
306+
...worktreeProperties,
307+
changes: []
308+
});
309+
291310
return [];
292311
}
293312

294-
const changes = diff.map(change => {
295-
return new vscode.ChatSessionChangedFile2(
296-
change.uri,
297-
change.status !== 1 /* INDEX_ADDED */
298-
? toGitUri(change.originalUri, worktreeProperties.baseCommit)
299-
: undefined,
300-
change.status !== 6 /* DELETED */
301-
? toGitUri(change.uri, worktreeProperties.branchName)
302-
: undefined,
303-
change.insertions,
304-
change.deletions);
313+
const changes = diff.map(change => ({
314+
filePath: change.uri.fsPath,
315+
originalFilePath: change.status !== 1 /* INDEX_ADDED */
316+
? change.originalUri?.fsPath
317+
: undefined,
318+
modifiedFilePath: change.status !== 6 /* DELETED */
319+
? change.uri.fsPath
320+
: undefined,
321+
statistics: {
322+
additions: change.insertions,
323+
deletions: change.deletions
324+
}
325+
} satisfies ChatSessionWorktreeFile));
326+
327+
this.setWorktreeProperties(sessionId, {
328+
...worktreeProperties, changes
305329
});
306330

307-
this._sessionWorktreeChanges.set(sessionId, changes);
308331
return changes;
309332
}
310333

@@ -340,7 +363,10 @@ export class ChatSessionWorktreeService extends Disposable implements IChatSessi
340363
await this.gitService.commit(vscode.Uri.file(worktreePath), message, { all: true, noVerify: true, signCommit: false });
341364
this.logService.trace(`[ChatSessionWorktreeService] Committed all changes in working directory ${worktreePath}`);
342365

343-
// Delete worktree changes from cache
344-
this._sessionWorktreeChanges.delete(sessionId);
366+
// Delete worktree changes cache
367+
this.setWorktreeProperties(sessionId, {
368+
...worktreeProperties,
369+
changes: undefined
370+
});
345371
}
346372
}

src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ChatExtendedRequestHandler, ChatSessionProviderOptionItem, Uri } from '
1010
import { IRunCommandExecutionService } from '../../../platform/commands/common/runCommandExecutionService';
1111
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
1212
import { IGitService, RepoContext } from '../../../platform/git/common/gitService';
13+
import { toGitUri } from '../../../platform/git/common/utils';
1314
import { ILogService } from '../../../platform/log/common/logService';
1415
import { IPromptsService, ParsedPromptFile } from '../../../platform/promptFiles/common/promptsService';
1516
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
@@ -189,7 +190,20 @@ export class CopilotCLIChatSessionItemProvider extends Disposable implements vsc
189190
}
190191

191192
// Statistics
192-
const changes = await this.worktreeManager.getWorktreeChanges(session.id);
193+
const changes: vscode.ChatSessionChangedFile2[] = [];
194+
if (worktreeProperties) {
195+
const worktreeChanges = await this.worktreeManager.getWorktreeChanges(session.id) ?? [];
196+
changes.push(...worktreeChanges.map(change => new vscode.ChatSessionChangedFile2(
197+
vscode.Uri.file(change.filePath),
198+
change.originalFilePath
199+
? toGitUri(vscode.Uri.file(change.originalFilePath), worktreeProperties.baseCommit)
200+
: undefined,
201+
change.modifiedFilePath
202+
? toGitUri(vscode.Uri.file(change.modifiedFilePath), worktreeProperties.branchName)
203+
: undefined,
204+
change.statistics.additions,
205+
change.statistics.deletions)));
206+
}
193207

194208
// Status
195209
const status = session.status ?? vscode.ChatSessionStatus.Completed;
@@ -365,6 +379,13 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements
365379
// We need this when we create the session later for execution.
366380
_sessionModel.set(copilotcliSessionId, model);
367381

382+
// Ensure that the repository for the background session is opened. This is needed
383+
// when the background session is opened in the empty window so that we can access
384+
// the changes of the background session.
385+
if (worktreeProperties?.repositoryPath) {
386+
await this.gitService.getRepository(vscode.Uri.file(worktreeProperties.repositoryPath));
387+
}
388+
368389
return {
369390
history,
370391
activeResponseCallback: undefined,

0 commit comments

Comments
 (0)