Skip to content

Commit 3537bbc

Browse files
authored
Merge pull request microsoft#298072 from microsoft/copilot-worktree-2026-02-26T18-09-45
Sessions window: apply changes to parent repo
2 parents 406db26 + 7a46d11 commit 3537bbc

3 files changed

Lines changed: 164 additions & 0 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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 { Disposable } from '../../../../base/common/lifecycle.js';
7+
import { autorun } from '../../../../base/common/observable.js';
8+
import { Codicon } from '../../../../base/common/codicons.js';
9+
import { localize, localize2 } from '../../../../nls.js';
10+
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
11+
import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
12+
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
13+
import { IFileService } from '../../../../platform/files/common/files.js';
14+
import { INotificationService } from '../../../../platform/notification/common/notification.js';
15+
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
16+
import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js';
17+
import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js';
18+
import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js';
19+
import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';
20+
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
21+
import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js';
22+
import { isEqualOrParent, joinPath, relativePath } from '../../../../base/common/resources.js';
23+
import { ILogService } from '../../../../platform/log/common/log.js';
24+
import { URI } from '../../../../base/common/uri.js';
25+
26+
/**
27+
* Normalizes a URI to the `file` scheme so that path comparisons work
28+
* even when the source URI uses a different scheme (e.g. `github-remote-file`).
29+
*/
30+
function toFileUri(uri: URI): URI {
31+
return uri.scheme === 'file' ? uri : URI.file(uri.path);
32+
}
33+
34+
const hasWorktreeAndRepositoryContextKey = new RawContextKey<boolean>('agentSessionHasWorktreeAndRepository', false, {
35+
type: 'boolean',
36+
description: localize('agentSessionHasWorktreeAndRepository', "True when the active agent session has both a worktree and a parent repository.")
37+
});
38+
39+
class ApplyToParentRepoContribution extends Disposable implements IWorkbenchContribution {
40+
41+
static readonly ID = 'sessions.contrib.applyToParentRepo';
42+
43+
constructor(
44+
@IContextKeyService contextKeyService: IContextKeyService,
45+
@ISessionsManagementService sessionManagementService: ISessionsManagementService,
46+
) {
47+
super();
48+
49+
const contextKey = hasWorktreeAndRepositoryContextKey.bindTo(contextKeyService);
50+
51+
this._register(autorun(reader => {
52+
const activeSession = sessionManagementService.activeSession.read(reader);
53+
const hasWorktreeAndRepo = !!activeSession?.worktree && !!activeSession?.repository;
54+
contextKey.set(hasWorktreeAndRepo);
55+
}));
56+
}
57+
}
58+
59+
class ApplyToParentRepoAction extends Action2 {
60+
static readonly ID = 'chatEditing.applyToParentRepo';
61+
62+
constructor() {
63+
super({
64+
id: ApplyToParentRepoAction.ID,
65+
title: localize2('applyToParentRepo', 'Apply to Parent Repo'),
66+
icon: Codicon.desktopDownload,
67+
category: CHAT_CATEGORY,
68+
precondition: ContextKeyExpr.and(IsSessionsWindowContext, hasWorktreeAndRepositoryContextKey, ChatContextKeys.hasAgentSessionChanges),
69+
menu: [
70+
{
71+
id: MenuId.ChatEditingSessionChangesToolbar,
72+
group: 'navigation',
73+
order: 4,
74+
when: ContextKeyExpr.and(IsSessionsWindowContext, hasWorktreeAndRepositoryContextKey, ChatContextKeys.hasAgentSessionChanges),
75+
},
76+
],
77+
});
78+
}
79+
80+
override async run(accessor: ServicesAccessor): Promise<void> {
81+
const sessionManagementService = accessor.get(ISessionsManagementService);
82+
const agentSessionsService = accessor.get(IAgentSessionsService);
83+
const fileService = accessor.get(IFileService);
84+
const notificationService = accessor.get(INotificationService);
85+
const logService = accessor.get(ILogService);
86+
87+
const activeSession = sessionManagementService.getActiveSession();
88+
if (!activeSession?.worktree || !activeSession?.repository) {
89+
return;
90+
}
91+
92+
const worktreeRoot = activeSession.worktree;
93+
const repoRoot = activeSession.repository;
94+
95+
const agentSession = agentSessionsService.getSession(activeSession.resource);
96+
const changes = agentSession?.changes;
97+
if (!changes || !(changes instanceof Array)) {
98+
return;
99+
}
100+
101+
let copiedCount = 0;
102+
let deletedCount = 0;
103+
let errorCount = 0;
104+
105+
for (const change of changes) {
106+
try {
107+
const modifiedUri = isIChatSessionFileChange2(change)
108+
? change.modifiedUri ?? change.uri
109+
: change.modifiedUri;
110+
const isDeletion = isIChatSessionFileChange2(change)
111+
? change.modifiedUri === undefined
112+
: false;
113+
114+
if (isDeletion) {
115+
const originalUri = change.originalUri;
116+
if (originalUri && isEqualOrParent(toFileUri(originalUri), worktreeRoot)) {
117+
const relPath = relativePath(worktreeRoot, toFileUri(originalUri));
118+
if (relPath) {
119+
const targetUri = joinPath(repoRoot, relPath);
120+
if (await fileService.exists(targetUri)) {
121+
await fileService.del(targetUri);
122+
deletedCount++;
123+
}
124+
}
125+
}
126+
} else {
127+
if (isEqualOrParent(toFileUri(modifiedUri), worktreeRoot)) {
128+
const relPath = relativePath(worktreeRoot, toFileUri(modifiedUri));
129+
if (relPath) {
130+
const targetUri = joinPath(repoRoot, relPath);
131+
await fileService.copy(modifiedUri, targetUri, true);
132+
copiedCount++;
133+
}
134+
}
135+
}
136+
} catch (err) {
137+
logService.error('[ApplyToParentRepo] Failed to apply change', err);
138+
errorCount++;
139+
}
140+
}
141+
142+
const totalApplied = copiedCount + deletedCount;
143+
if (errorCount > 0) {
144+
notificationService.warn(
145+
totalApplied === 1
146+
? localize('applyToParentRepoPartial1', "Applied 1 file to parent repo with {0} error(s).", errorCount)
147+
: localize('applyToParentRepoPartialN', "Applied {0} files to parent repo with {1} error(s).", totalApplied, errorCount)
148+
);
149+
} else if (totalApplied > 0) {
150+
notificationService.info(
151+
totalApplied === 1
152+
? localize('applyToParentRepoSuccess1', "Applied 1 file to parent repo.")
153+
: localize('applyToParentRepoSuccessN', "Applied {0} files to parent repo.", totalApplied)
154+
);
155+
}
156+
}
157+
}
158+
159+
registerAction2(ApplyToParentRepoAction);
160+
registerWorkbenchContribution2(ApplyToParentRepoContribution.ID, ApplyToParentRepoContribution, WorkbenchPhase.AfterRestored);

src/vs/sessions/contrib/changesView/browser/changesView.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,9 @@ export class ChangesViewPane extends ViewPane {
565565
if (action.id === 'github.createPullRequest') {
566566
return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow' };
567567
}
568+
if (action.id === 'chatEditing.applyToParentRepo') {
569+
return { showIcon: true, showLabel: false, isSecondary: true };
570+
}
568571
if (action.id === 'chatEditing.synchronizeChanges') {
569572
return { showIcon: true, showLabel: true, isSecondary: true };
570573
}

src/vs/sessions/sessions.desktop.main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ import './contrib/sessions/browser/sessions.contribution.js';
207207
import './contrib/sessions/browser/customizationsToolbar.contribution.js';
208208
import './contrib/changesView/browser/changesView.contribution.js';
209209
import './contrib/gitSync/browser/gitSync.contribution.js';
210+
import './contrib/applyToParentRepo/browser/applyToParentRepo.contribution.js';
210211
import './contrib/fileTreeView/browser/fileTreeView.contribution.js'; // view registration disabled; filesystem provider still needed
211212
import './contrib/configuration/browser/configuration.contribution.js';
212213

0 commit comments

Comments
 (0)