Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts
13401 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 { derived, IObservable, observableValue, ISettableObservable } from '../../../../base/common/observable.js';
7
import { relativePath } from '../../../../base/common/resources.js';
8
import { URI } from '../../../../base/common/uri.js';
9
import { CancellationToken } from '../../../../base/common/cancellation.js';
10
import { IAICustomizationWorkspaceService, AICustomizationManagementSection, IStorageSourceFilter, applyStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
11
import { IChatPromptSlashCommand, IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
12
import { ICustomizationHarnessService } from '../../../../workbench/contrib/chat/common/customizationHarnessService.js';
13
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
14
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
15
import { CustomizationCreatorService } from '../../../../workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.js';
16
import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';
17
import { ICommandService } from '../../../../platform/commands/common/commands.js';
18
import { ILogService } from '../../../../platform/log/common/log.js';
19
import { IFileService } from '../../../../platform/files/common/files.js';
20
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
21
import { localize } from '../../../../nls.js';
22
import { AGENT_HOST_SCHEME } from '../../../../platform/agentHost/common/agentHostUri.js';
23
24
/**
25
* Agent Sessions override of IAICustomizationWorkspaceService.
26
* Delegates to ISessionsManagementService to provide the active session's
27
* worktree/repository as the project root, and supports worktree commit.
28
*
29
* Customization files are always committed to the main repository so they
30
* persist across worktrees. When a worktree is active the file is also
31
* copied into the worktree and committed there so the running session
32
* picks it up immediately.
33
*/
34
export class SessionsAICustomizationWorkspaceService implements IAICustomizationWorkspaceService {
35
declare readonly _serviceBrand: undefined;
36
37
readonly activeProjectRoot: IObservable<URI | undefined>;
38
readonly hasOverrideProjectRoot: IObservable<boolean>;
39
40
/**
41
* Transient override for the project root. When set, `activeProjectRoot`
42
* returns this value instead of the session-derived root.
43
*/
44
private readonly _overrideRoot: ISettableObservable<URI | undefined>;
45
46
constructor(
47
@ISessionsManagementService private readonly sessionsService: ISessionsManagementService,
48
@IInstantiationService private readonly instantiationService: IInstantiationService,
49
@IPromptsService private readonly promptsService: IPromptsService,
50
@ICustomizationHarnessService private readonly harnessService: ICustomizationHarnessService,
51
@ICommandService private readonly commandService: ICommandService,
52
@ILogService private readonly logService: ILogService,
53
@IFileService private readonly fileService: IFileService,
54
@INotificationService private readonly notificationService: INotificationService,
55
) {
56
this._overrideRoot = observableValue(this, undefined);
57
58
this.activeProjectRoot = derived(reader => {
59
const override = this._overrideRoot.read(reader);
60
if (override) {
61
return override;
62
}
63
const session = this.sessionsService.activeSession.read(reader);
64
const repo = session?.workspace.read(reader)?.repositories[0];
65
const root = repo?.workingDirectory ?? repo?.uri;
66
if (root?.scheme === AGENT_HOST_SCHEME) {
67
return undefined;
68
}
69
return root;
70
});
71
72
this.hasOverrideProjectRoot = derived(reader => {
73
return this._overrideRoot.read(reader) !== undefined;
74
});
75
}
76
77
getActiveProjectRoot(): URI | undefined {
78
const override = this._overrideRoot.get();
79
if (override) {
80
return override;
81
}
82
const session = this.sessionsService.activeSession.get();
83
const repo = session?.workspace.get()?.repositories[0];
84
const root = repo?.workingDirectory ?? repo?.uri;
85
if (root?.scheme === AGENT_HOST_SCHEME) {
86
return undefined;
87
}
88
return root;
89
}
90
91
setOverrideProjectRoot(root: URI): void {
92
this._overrideRoot.set(root, undefined);
93
}
94
95
clearOverrideProjectRoot(): void {
96
this._overrideRoot.set(undefined, undefined);
97
}
98
99
readonly managementSections: readonly AICustomizationManagementSection[] = [
100
AICustomizationManagementSection.Agents,
101
AICustomizationManagementSection.Skills,
102
AICustomizationManagementSection.Instructions,
103
AICustomizationManagementSection.Hooks,
104
AICustomizationManagementSection.McpServers,
105
AICustomizationManagementSection.Plugins,
106
];
107
108
getStorageSourceFilter(type: PromptsType): IStorageSourceFilter {
109
return this.harnessService.getStorageSourceFilter(type);
110
}
111
112
readonly isSessionsWindow = true;
113
114
readonly welcomePageFeatures = {
115
showGettingStartedBanner: true,
116
};
117
118
/**
119
* Commits customization files. Always commits to the main repository
120
* so the change persists across worktrees. When a worktree is active
121
* the file is also committed there so the session sees it immediately.
122
*/
123
async commitFiles(_projectRoot: URI, fileUris: URI[]): Promise<void> {
124
const session = this.sessionsService.activeSession.get();
125
const repo = session?.workspace.get()?.repositories[0];
126
if (!repo?.uri) {
127
return;
128
}
129
130
for (const fileUri of fileUris) {
131
await this.commitFileToRepos(fileUri, repo.uri, repo.workingDirectory);
132
}
133
}
134
135
/**
136
* Commits the deletion of files that have already been removed from disk.
137
* Always stages + commits the removal in the main repository, and also
138
* in the worktree if one is active.
139
*/
140
async deleteFiles(_projectRoot: URI, fileUris: URI[]): Promise<void> {
141
const session = this.sessionsService.activeSession.get();
142
const repo = session?.workspace.get()?.repositories[0];
143
if (!repo?.uri) {
144
return;
145
}
146
147
for (const fileUri of fileUris) {
148
await this.commitDeletionToRepos(fileUri, repo.uri, repo.workingDirectory);
149
}
150
}
151
152
/**
153
* Computes the repository-relative path for a file. The file may be
154
* located under the worktree or the repository root.
155
*/
156
private getRelativePath(fileUri: URI, repositoryUri: URI, worktreeUri: URI | undefined): string | undefined {
157
// Try worktree first (when active, files are written under it)
158
if (worktreeUri) {
159
const rel = relativePath(worktreeUri, fileUri);
160
if (rel) {
161
return rel;
162
}
163
}
164
return relativePath(repositoryUri, fileUri);
165
}
166
167
/**
168
* Commits a single file to the main repository and optionally the worktree.
169
* Copies the file content between trees when needed.
170
*/
171
private async commitFileToRepos(fileUri: URI, repositoryUri: URI, worktreeUri: URI | undefined): Promise<void> {
172
const relPath = this.getRelativePath(fileUri, repositoryUri, worktreeUri);
173
if (!relPath) {
174
return;
175
}
176
177
const repoFileUri = URI.joinPath(repositoryUri, relPath);
178
179
// 1. Always commit to main repository
180
try {
181
if (repoFileUri.toString() !== fileUri.toString()) {
182
const content = await this.fileService.readFile(fileUri);
183
await this.fileService.writeFile(repoFileUri, content.value);
184
}
185
await this.commandService.executeCommand(
186
'github.copilot.cli.sessions.commitToRepository',
187
{ repositoryUri, fileUri: repoFileUri }
188
);
189
} catch (error) {
190
this.logService.error('[SessionsAICustomizationWorkspaceService] Failed to commit to repository:', error);
191
if (worktreeUri) {
192
this.notificationService.notify({
193
severity: Severity.Warning,
194
message: localize('commitToRepoFailed', "Your customization was saved to this session's worktree, but we couldn't apply it to the default branch. You may need to apply it manually."),
195
});
196
}
197
}
198
199
// 2. Also commit to the worktree if active
200
if (worktreeUri) {
201
const worktreeFileUri = URI.joinPath(worktreeUri, relPath);
202
try {
203
if (worktreeFileUri.toString() !== fileUri.toString()) {
204
const content = await this.fileService.readFile(fileUri);
205
await this.fileService.writeFile(worktreeFileUri, content.value);
206
}
207
await this.commandService.executeCommand(
208
'github.copilot.cli.sessions.commitToWorktree',
209
{ worktreeUri, fileUri: worktreeFileUri }
210
);
211
} catch (error) {
212
this.logService.error('[SessionsAICustomizationWorkspaceService] Failed to commit to worktree:', error);
213
}
214
}
215
}
216
217
/**
218
* Commits the deletion of a file to the main repository and optionally
219
* the worktree. The file is already deleted from disk before this is called;
220
* `git add` on a deleted path stages the removal.
221
*/
222
private async commitDeletionToRepos(fileUri: URI, repositoryUri: URI, worktreeUri: URI | undefined): Promise<void> {
223
const relPath = this.getRelativePath(fileUri, repositoryUri, worktreeUri);
224
if (!relPath) {
225
return;
226
}
227
228
const repoFileUri = URI.joinPath(repositoryUri, relPath);
229
230
// 1. Delete from main repository if it exists there, then commit
231
try {
232
if (await this.fileService.exists(repoFileUri)) {
233
await this.fileService.del(repoFileUri, { useTrash: true, recursive: true });
234
}
235
await this.commandService.executeCommand(
236
'github.copilot.cli.sessions.commitToRepository',
237
{ repositoryUri, fileUri: repoFileUri }
238
);
239
} catch (error) {
240
this.logService.error('[SessionsAICustomizationWorkspaceService] Failed to commit deletion to repository:', error);
241
if (worktreeUri) {
242
this.notificationService.notify({
243
severity: Severity.Warning,
244
message: localize('deleteFromRepoFailed', "Your customization was removed from this session's worktree, but we couldn't apply the change to the default branch. You may need to remove it manually."),
245
});
246
}
247
}
248
249
// 2. Also commit the deletion in the worktree if active
250
if (worktreeUri) {
251
const worktreeFileUri = URI.joinPath(worktreeUri, relPath);
252
try {
253
// The file may already be deleted from the worktree by the caller
254
await this.commandService.executeCommand(
255
'github.copilot.cli.sessions.commitToWorktree',
256
{ worktreeUri, fileUri: worktreeFileUri }
257
);
258
} catch (error) {
259
this.logService.error('[SessionsAICustomizationWorkspaceService] Failed to commit deletion to worktree:', error);
260
}
261
}
262
}
263
264
async generateCustomization(type: PromptsType): Promise<void> {
265
const creator = this.instantiationService.createInstance(CustomizationCreatorService);
266
await creator.createWithAI(type);
267
}
268
269
async getFilteredPromptSlashCommands(token: CancellationToken): Promise<readonly IChatPromptSlashCommand[]> {
270
const allCommands = await this.promptsService.getPromptSlashCommands(token);
271
return allCommands.filter(cmd => {
272
const filter = this.getStorageSourceFilter(cmd.type);
273
return applyStorageSourceFilter([cmd], filter).length > 0;
274
});
275
}
276
277
private static readonly _skillUIIntegrations: ReadonlyMap<string, string> = new Map([
278
['act-on-feedback', localize('skillUI.actOnFeedback', "Used by the Submit Feedback button in the Changes toolbar")],
279
['generate-run-commands', localize('skillUI.generateRunCommands', "Used by the Run button in the title bar")],
280
['create-pr', localize('skillUI.createPr', "Used by the Create Pull Request button in the Changes toolbar")],
281
['create-draft-pr', localize('skillUI.createDraftPr', "Used by the Create Draft Pull Request button in the Changes toolbar")],
282
['update-pr', localize('skillUI.updatePr', "Used by the Update Pull Request button in the Changes toolbar")],
283
['merge-changes', localize('skillUI.mergeChanges', "Used by the Merge button in the Changes toolbar")],
284
['commit', localize('skillUI.commit', "Used by the Commit button in the Changes toolbar")],
285
]);
286
287
getSkillUIIntegrations(): ReadonlyMap<string, string> {
288
return SessionsAICustomizationWorkspaceService._skillUIIntegrations;
289
}
290
}
291
292