Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4403,7 +4403,7 @@
"chat/input/editing/sessionToolbar": [
{
"command": "github.copilot.chat.applyCopilotCLIAgentSessionChanges.apply",
"when": "chatSessionType == copilotcli",
"when": "chatSessionType == copilotcli && workbenchState != empty",
"group": "navigation@0"
},
{
Expand Down Expand Up @@ -4973,7 +4973,7 @@
"multiDiffEditor/content": [
{
"command": "github.copilot.chat.applyCopilotCLIAgentSessionChanges",
"when": "resourceScheme == copilotcli-worktree-changes"
"when": "resourceScheme == copilotcli-worktree-changes && workbenchState != empty"
}
],
"chat/chatSessions": [
Expand All @@ -4999,7 +4999,7 @@
},
{
"command": "github.copilot.chat.applyCopilotCLIAgentSessionChanges",
"when": "chatSessionType == copilotcli",
"when": "chatSessionType == copilotcli && workbenchState != empty",
"group": "3_apply@0"
},
{
Expand Down
13 changes: 12 additions & 1 deletion src/extension/chatSessions/common/chatSessionWorktreeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ import type * as vscode from 'vscode';
import { RepoContext } from '../../../platform/git/common/gitService';
import { createServiceIdentifier } from '../../../util/common/services';

export interface ChatSessionWorktreeFile {
readonly filePath: string;
readonly originalFilePath: string | undefined;
readonly modifiedFilePath: string | undefined;
readonly statistics: {
readonly additions: number;
readonly deletions: number;
};
}

export interface ChatSessionWorktreeData {
readonly data: string;
readonly version: number;
Expand All @@ -18,6 +28,7 @@ interface ChatSessionWorktreePropertiesV1 {
readonly branchName: string;
readonly repositoryPath: string;
readonly worktreePath: string;
readonly changes?: readonly ChatSessionWorktreeFile[] | undefined;
}

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

applyWorktreeChanges(sessionId: string): Promise<void>;
getWorktreeChanges(sessionId: string): Promise<vscode.ChatSessionChangedFile2[] | undefined>;
getWorktreeChanges(sessionId: string): Promise<readonly ChatSessionWorktreeFile[] | undefined>;

handleRequestCompleted(sessionId: string): Promise<void>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,19 @@ import { CancellationToken } from 'vscode-languageserver-protocol';
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
import { IGitCommitMessageService } from '../../../platform/git/common/gitCommitMessageService';
import { IGitService, RepoContext } from '../../../platform/git/common/gitService';
import { toGitUri } from '../../../platform/git/common/utils';
import { ILogService } from '../../../platform/log/common/logService';
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
import { Disposable } from '../../../util/vs/base/common/lifecycle';
import * as path from '../../../util/vs/base/common/path';
import { basename, isEqual } from '../../../util/vs/base/common/resources';
import { ChatSessionWorktreeData, ChatSessionWorktreeProperties, IChatSessionWorktreeService } from '../common/chatSessionWorktreeService';
import { ChatSessionWorktreeData, ChatSessionWorktreeFile, ChatSessionWorktreeProperties, IChatSessionWorktreeService } from '../common/chatSessionWorktreeService';

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

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

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

constructor(
@IGitCommitMessageService private readonly gitCommitMessageService: IGitCommitMessageService,
Expand Down Expand Up @@ -165,8 +163,13 @@ export class ChatSessionWorktreeService extends Disposable implements IChatSessi
untracked: true
});

// Clear worktree changes cache
this._sessionWorktreeChanges.delete(sessionId);
// Delete worktree changes cache
if (worktreeProperties) {
this.setWorktreeProperties(sessionId, {
...worktreeProperties,
changes: undefined
});
}

return;
}
Expand Down Expand Up @@ -211,70 +214,82 @@ export class ChatSessionWorktreeService extends Disposable implements IChatSessi
});
}

// Clear worktree changes cache
this._sessionWorktreeChanges.delete(sessionId);
// Delete worktree changes cache
this.setWorktreeProperties(sessionId, {
...worktreeProperties,
changes: undefined
});
}

async getWorktreeChanges(sessionId: string): Promise<vscode.ChatSessionChangedFile2[] | undefined> {
if (this._sessionWorktreeChanges.has(sessionId)) {
return this._sessionWorktreeChanges.get(sessionId);
async getWorktreeChanges(sessionId: string): Promise<readonly ChatSessionWorktreeFile[] | undefined> {
// Get worktree properties
const worktreeProperties = this.getWorktreeProperties(sessionId);
if (!worktreeProperties) {
return undefined;
}

// Check whether the session has an associated worktree
const worktreePath = this.getWorktreePath(sessionId);
if (!worktreePath) {
return undefined;
// Return cached changes
if (worktreeProperties.changes) {
return worktreeProperties.changes;
}

const worktreePath = vscode.Uri.file(worktreeProperties.worktreePath);

// Ensure the initial repository discovery is completed and the repository
// states are initialized in the vscode.git extension. This is needed as these
// will be the repositories that we use to compute the worktree changes. We do
// not have to open each worktree individually since the changes are committed
// so we can get them from the main repository or discovered worktree.
await this.gitService.initialize();

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

if (!repository) {
this._sessionWorktreeChanges.set(sessionId, undefined);
return undefined;
}

// Get worktree properties
const worktreeProperties = this.getWorktreeProperties(sessionId);

if (worktreeProperties === undefined || worktreeProperties.autoCommit === false) {
if (worktreeProperties.autoCommit === false) {
// These changes are staged in the worktree but not yet committed. Since the
// changes are not committed, we need to get them from the worktree repository
// state. To do that we need to open the worktree repository. The source control
// provider will not be shown in the Source Control view since it is being hidden.
const worktreeRepository = await this.gitService.getRepository(worktreePath);

if (!worktreeRepository?.changes) {
this._sessionWorktreeChanges.set(sessionId, []);
this.setWorktreeProperties(sessionId, {
...worktreeProperties,
changes: []
});

return [];
}

const changes: vscode.ChatSessionChangedFile2[] = [];
const changes: ChatSessionWorktreeFile[] = [];
for (const change of [...worktreeRepository.changes.indexChanges, ...worktreeRepository.changes.workingTree]) {
try {
const fileStats = await this.gitService.diffIndexWithHEADShortStats(change.uri);
changes.push(new vscode.ChatSessionChangedFile2(
change.uri,
change.status !== 1 /* INDEX_ADDED */
? change.originalUri
changes.push({
filePath: change.uri.fsPath,
originalFilePath: change.status !== 1 /* INDEX_ADDED */
? change.originalUri?.fsPath
: undefined,
change.status !== 2 /* INDEX_DELETED */
? change.uri
modifiedFilePath: change.status !== 2 /* INDEX_DELETED */
? change.uri.fsPath
: undefined,
fileStats?.insertions ?? 0,
fileStats?.deletions ?? 0));
statistics: {
additions: fileStats?.insertions ?? 0,
deletions: fileStats?.deletions ?? 0
}
} satisfies ChatSessionWorktreeFile);
} catch (error) { }
}

this._sessionWorktreeChanges.set(sessionId, changes);
this.setWorktreeProperties(sessionId, {
...worktreeProperties, changes
});
return changes;
}

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

if (!diff) {
this._sessionWorktreeChanges.set(sessionId, []);
this.setWorktreeProperties(sessionId, {
...worktreeProperties,
changes: []
});

return [];
}

const changes = diff.map(change => {
return new vscode.ChatSessionChangedFile2(
change.uri,
change.status !== 1 /* INDEX_ADDED */
? toGitUri(change.originalUri, worktreeProperties.baseCommit)
: undefined,
change.status !== 6 /* DELETED */
? toGitUri(change.uri, worktreeProperties.branchName)
: undefined,
change.insertions,
change.deletions);
const changes = diff.map(change => ({
filePath: change.uri.fsPath,
originalFilePath: change.status !== 1 /* INDEX_ADDED */
? change.originalUri?.fsPath
: undefined,
modifiedFilePath: change.status !== 6 /* DELETED */
? change.uri.fsPath
: undefined,
statistics: {
additions: change.insertions,
deletions: change.deletions
}
} satisfies ChatSessionWorktreeFile));

this.setWorktreeProperties(sessionId, {
...worktreeProperties, changes
});

this._sessionWorktreeChanges.set(sessionId, changes);
return changes;
}

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

// Delete worktree changes from cache
this._sessionWorktreeChanges.delete(sessionId);
// Delete worktree changes cache
this.setWorktreeProperties(sessionId, {
...worktreeProperties,
changes: undefined
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ChatExtendedRequestHandler, ChatSessionProviderOptionItem, Uri } from '
import { IRunCommandExecutionService } from '../../../platform/commands/common/runCommandExecutionService';
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
import { IGitService, RepoContext } from '../../../platform/git/common/gitService';
import { toGitUri } from '../../../platform/git/common/utils';
import { ILogService } from '../../../platform/log/common/logService';
import { IPromptsService, ParsedPromptFile } from '../../../platform/promptFiles/common/promptsService';
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
Expand Down Expand Up @@ -189,7 +190,20 @@ export class CopilotCLIChatSessionItemProvider extends Disposable implements vsc
}

// Statistics
const changes = await this.worktreeManager.getWorktreeChanges(session.id);
const changes: vscode.ChatSessionChangedFile2[] = [];
if (worktreeProperties) {
const worktreeChanges = await this.worktreeManager.getWorktreeChanges(session.id) ?? [];
changes.push(...worktreeChanges.map(change => new vscode.ChatSessionChangedFile2(
vscode.Uri.file(change.filePath),
change.originalFilePath
? toGitUri(vscode.Uri.file(change.originalFilePath), worktreeProperties.baseCommit)
: undefined,
change.modifiedFilePath
? toGitUri(vscode.Uri.file(change.modifiedFilePath), worktreeProperties.branchName)
: undefined,
change.statistics.additions,
change.statistics.deletions)));
}

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

// Ensure that the repository for the background session is opened. This is needed
// when the background session is opened in the empty window so that we can access
// the changes of the background session.
if (worktreeProperties?.repositoryPath) {
await this.gitService.getRepository(vscode.Uri.file(worktreeProperties.repositoryPath));
}

return {
history,
activeResponseCallback: undefined,
Expand Down