Skip to content

Commit 0e38108

Browse files
benibenjCopilot
andcommitted
Explain Agent Merge behavior in chat
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8d7c38f commit 0e38108

8 files changed

Lines changed: 391 additions & 14 deletions

File tree

src/vs/platform/agentHost/common/agentMerge.ts

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,122 @@ export const agentMergeDisableReasons = {
306306
} as const;
307307

308308
/** The transcript notice shown once Agent Merge starts watching a branch. */
309-
export function agentMergeEnabledNotice(branchName: string): string {
310-
return localize('agentMerge.notice.enabled', "Agent Merge is on and watching {0}.", appendEscapedMarkdownInlineCode(branchName));
309+
export function agentMergeEnabledNotice(target: Pick<AgentMergeTarget, 'branchName' | 'pullRequestUrl'>, configuration: AgentMergeConfiguration): string {
310+
const lines = [
311+
target.pullRequestUrl
312+
? localize('agentMerge.notice.enabled.withPullRequest', "Agent Merge is on for {0} and is monitoring its pull request.", appendEscapedMarkdownInlineCode(target.branchName))
313+
: localize('agentMerge.notice.enabled', "Agent Merge is on for {0}. It will wait for a pull request on this branch, then monitor it.", appendEscapedMarkdownInlineCode(target.branchName)),
314+
];
315+
if (configuration.addressReviews) {
316+
lines.push(localize('agentMerge.notice.enabled.addressReviews', "It will ask the agent to address new pull request review comments."));
317+
}
318+
if (configuration.fixCI) {
319+
lines.push(localize('agentMerge.notice.enabled.fixCI', "It will ask the agent to fix failing CI checks."));
320+
}
321+
if (configuration.resolveConflicts) {
322+
lines.push(localize('agentMerge.notice.enabled.resolveConflicts', "It will ask the agent to resolve merge conflicts and update the branch when it falls behind."));
323+
}
324+
if (!configuration.addressReviews && !configuration.fixCI && !configuration.resolveConflicts) {
325+
lines.push(localize('agentMerge.notice.enabled.noRepairs', "It will monitor the pull request but will not ask the agent to repair blockers."));
326+
}
327+
if (configuration.addressReviews) {
328+
lines.push(configuration.replyAttribution
329+
? localize('agentMerge.notice.enabled.replyAttribution', "Replies it posts will identify Agent Merge as the source.")
330+
: localize('agentMerge.notice.enabled.noReplyAttribution', "Replies it posts will not identify Agent Merge as the source."));
331+
}
332+
lines.push(
333+
configuration.addressReviews
334+
? localize('agentMerge.notice.enabled.waiting', "After each update, it will wait for new CI results and review comments.")
335+
: localize('agentMerge.notice.enabled.waitingForCI', "After each update, it will wait for new CI results."),
336+
agentMergeMergeBehaviorNotice(configuration.mergePullRequest),
337+
);
338+
if (configuration.mergePullRequest !== 'never') {
339+
lines.push(agentMergeMergeMethodNotice(configuration.mergeMethod));
340+
}
341+
return lines.join(' ');
342+
}
343+
344+
/** The transcript notice shown when effective Agent Merge behavior changes. */
345+
export function agentMergeConfigurationChangedNotice(previous: AgentMergeConfiguration, current: AgentMergeConfiguration): string | undefined {
346+
const changes: string[] = [];
347+
if (previous.addressReviews !== current.addressReviews) {
348+
changes.push(current.addressReviews
349+
? localize('agentMerge.notice.configuration.addressReviews.enabled', "It will now address new pull request review comments.")
350+
: localize('agentMerge.notice.configuration.addressReviews.disabled', "It will no longer address new pull request review comments or wait for them before merging."));
351+
}
352+
if (previous.fixCI !== current.fixCI) {
353+
changes.push(current.fixCI
354+
? localize('agentMerge.notice.configuration.fixCI.enabled', "It will now fix failing CI checks.")
355+
: localize('agentMerge.notice.configuration.fixCI.disabled', "It will no longer fix failing CI checks."));
356+
}
357+
if (previous.resolveConflicts !== current.resolveConflicts) {
358+
changes.push(current.resolveConflicts
359+
? localize('agentMerge.notice.configuration.resolveConflicts.enabled', "It will now resolve merge conflicts and update the branch when it falls behind.")
360+
: localize('agentMerge.notice.configuration.resolveConflicts.disabled', "It will no longer resolve merge conflicts or update a behind branch."));
361+
}
362+
if (previous.mergePullRequest !== current.mergePullRequest) {
363+
changes.push(agentMergeMergeBehaviorChangedNotice(current.mergePullRequest));
364+
}
365+
if (current.mergePullRequest !== 'never'
366+
&& (previous.mergeMethod !== current.mergeMethod || previous.mergePullRequest === 'never')) {
367+
changes.push(agentMergeMergeMethodChangedNotice(current.mergeMethod));
368+
}
369+
if (previous.replyAttribution !== current.replyAttribution && current.addressReviews) {
370+
changes.push(current.replyAttribution
371+
? localize('agentMerge.notice.configuration.replyAttribution.enabled', "Replies it posts will now identify Agent Merge as the source.")
372+
: localize('agentMerge.notice.configuration.replyAttribution.disabled', "Replies it posts will no longer identify Agent Merge as the source."));
373+
}
374+
return changes.length > 0
375+
? [localize('agentMerge.notice.configuration.changed', "Agent Merge settings changed."), ...changes].join(' ')
376+
: undefined;
377+
}
378+
379+
function agentMergeMergeBehaviorNotice(mergePullRequest: AgentMergeMergePullRequest): string {
380+
switch (mergePullRequest) {
381+
case 'always':
382+
return localize('agentMerge.notice.merge.always', "When the pull request is ready, Agent Merge will merge it automatically.");
383+
case 'ifUnchanged':
384+
return localize('agentMerge.notice.merge.ifUnchanged', "When the pull request is ready, Agent Merge will merge it automatically only if it has not made changes.");
385+
case 'never':
386+
return localize('agentMerge.notice.merge.never', "It will not merge the pull request automatically and will keep monitoring it.");
387+
}
388+
}
389+
390+
function agentMergeMergeMethodChangedNotice(mergeMethod: AgentMergeMethod): string {
391+
switch (mergeMethod) {
392+
case 'auto':
393+
return localize('agentMerge.notice.configuration.mergeMethod.auto', "It will now choose an available merge method automatically.");
394+
case 'squash':
395+
return localize('agentMerge.notice.configuration.mergeMethod.squash', "It will now squash-merge the pull request.");
396+
case 'merge':
397+
return localize('agentMerge.notice.configuration.mergeMethod.merge', "It will now create a merge commit.");
398+
case 'rebase':
399+
return localize('agentMerge.notice.configuration.mergeMethod.rebase', "It will now rebase and merge the pull request.");
400+
}
401+
}
402+
403+
function agentMergeMergeMethodNotice(mergeMethod: AgentMergeMethod): string {
404+
switch (mergeMethod) {
405+
case 'auto':
406+
return localize('agentMerge.notice.mergeMethod.auto', "It will choose an available merge method automatically.");
407+
case 'squash':
408+
return localize('agentMerge.notice.mergeMethod.squash', "It will squash-merge the pull request.");
409+
case 'merge':
410+
return localize('agentMerge.notice.mergeMethod.merge', "It will create a merge commit.");
411+
case 'rebase':
412+
return localize('agentMerge.notice.mergeMethod.rebase', "It will rebase and merge the pull request.");
413+
}
414+
}
415+
416+
function agentMergeMergeBehaviorChangedNotice(mergePullRequest: AgentMergeMergePullRequest): string {
417+
switch (mergePullRequest) {
418+
case 'always':
419+
return localize('agentMerge.notice.configuration.merge.always', "It will now merge the pull request automatically when it is ready.");
420+
case 'ifUnchanged':
421+
return localize('agentMerge.notice.configuration.merge.ifUnchanged', "It will now merge the pull request when it is ready, but only if Agent Merge has not made changes.");
422+
case 'never':
423+
return localize('agentMerge.notice.configuration.merge.never', "It will no longer merge the pull request automatically.");
424+
}
311425
}
312426

313427
/** The transcript notice shown when the user, rather than the controller, turns Agent Merge off. */

src/vs/platform/agentHost/common/meta/agentSystemNotificationMeta.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export const enum AgentSystemNotificationKind {
77
WorktreeCreationFailure = 'worktreeCreationFailure',
88
/** Agent Merge started monitoring the session's branch. */
99
AgentMergeEnabled = 'agentMergeEnabled',
10+
/** Effective Agent Merge behavior changed while monitoring. */
11+
AgentMergeConfigurationChanged = 'agentMergeConfigurationChanged',
1012
/** Agent Merge stopped monitoring the session, usually on its own. */
1113
AgentMergeDisabled = 'agentMergeDisabled',
1214
}
@@ -18,6 +20,7 @@ export const enum AgentSystemNotificationSeverity {
1820
const knownKinds: ReadonlySet<string> = new Set<string>([
1921
AgentSystemNotificationKind.WorktreeCreationFailure,
2022
AgentSystemNotificationKind.AgentMergeEnabled,
23+
AgentSystemNotificationKind.AgentMergeConfigurationChanged,
2124
AgentSystemNotificationKind.AgentMergeDisabled,
2225
]);
2326

src/vs/platform/agentHost/node/agentMergeController.ts

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { IGitHubService } from '../../github/common/githubService.js';
1515
import { PullRequestRef, PullRequestSnapshot, PullRequestSubscription } from '../../github/common/githubPullRequestService.js';
1616
import { GitHubRequestError } from '../../github/common/githubTransport.js';
1717
import { ILogService } from '../../log/common/log.js';
18-
import { AgentMergeConfigKey, AgentMergeConfiguration, AgentMergeDisableReason, AgentMergeSessionState, AgentMergeTarget, AGENT_MERGE_UNKNOWN_COMMIT, agentMergeDisableReasons, agentMergeDisabledNotice, agentMergeEnabledNotice, agentMergeGateFragments, agentMergeMergePullRequestDemotedNotice, agentMergeRootConfigSchema, defaultAgentMergeConfiguration, evaluateAgentMerge, readAgentMergeSessionState, resolveAgentMergeConfiguration, resolveMergeMethod, shouldStopMergingAfterAgentChanges } from '../common/agentMerge.js';
18+
import { AgentMergeConfigKey, AgentMergeConfiguration, AgentMergeDisableReason, AgentMergeSessionState, AgentMergeTarget, AGENT_MERGE_UNKNOWN_COMMIT, agentMergeConfigurationChangedNotice, agentMergeDisableReasons, agentMergeDisabledNotice, agentMergeEnabledNotice, agentMergeGateFragments, agentMergeMergePullRequestDemotedNotice, agentMergeRootConfigSchema, defaultAgentMergeConfiguration, evaluateAgentMerge, readAgentMergeSessionState, resolveAgentMergeConfiguration, resolveMergeMethod, shouldStopMergingAfterAgentChanges } from '../common/agentMerge.js';
1919
import { buildAgentMergePrompt } from '../common/agentMergePrompt.js';
2020
import { IAgentHostGitStateService } from '../common/agentHostGitStateService.js';
2121
import { IAgentHostGitService } from '../common/agentHostGitService.js';
@@ -103,6 +103,7 @@ export class AgentMergeController extends Disposable {
103103
* sync that {@link _disable} triggers cannot post a second, reasonless one.
104104
*/
105105
private readonly _monitoredSessions = new Set<string>();
106+
private readonly _announcedConfigurations = new Map<string, AgentMergeConfiguration>();
106107

107108
constructor(
108109
private readonly _options: IAgentMergeControllerOptions,
@@ -123,6 +124,11 @@ export class AgentMergeController extends Disposable {
123124
return;
124125
}
125126
const session = event.session.toString();
127+
if (!previous?.enabled && current?.enabled && current.target) {
128+
this._postEnabledNotice(session, current);
129+
} else {
130+
this._postConfigurationChangedNotice(session, current);
131+
}
126132
if (this._resetRepairBaselineOnReselection(session, previous, current)) {
127133
// The reset re-enters this listener, which then syncs.
128134
return;
@@ -137,6 +143,7 @@ export class AgentMergeController extends Disposable {
137143
}));
138144
this._register(this._stateManager.onDidRemoveSession(session => {
139145
this._monitoredSessions.delete(session);
146+
this._announcedConfigurations.delete(session);
140147
this._stopRuntime(session);
141148
}));
142149
this._register(this._gitStateService.onDidRefreshSessionGitState(session => {
@@ -147,6 +154,8 @@ export class AgentMergeController extends Disposable {
147154
this._register(this._gitStateService.onDidChangeSessionGitHubState(session => this._schedule(session, 0)));
148155
this._register(this._configurationService.onDidRootConfigChange(() => {
149156
for (const session of this._stateManager.getSessionUris()) {
157+
const agentMerge = readAgentMergeSessionState(this._stateManager.getSessionState(session)?.config?.values);
158+
this._postConfigurationChangedNotice(session, agentMerge);
150159
this._syncSession(session);
151160
}
152161
}));
@@ -251,6 +260,7 @@ export class AgentMergeController extends Disposable {
251260
if (this._monitoredSessions.delete(session) && state) {
252261
this._postNotice(session, AgentSystemNotificationKind.AgentMergeDisabled, agentMergeDisabledNotice());
253262
}
263+
this._announcedConfigurations.delete(session);
254264
if (agentMerge?.injectedConfiguration) {
255265
this._restoreInjectedConfiguration(session, agentMerge);
256266
}
@@ -289,6 +299,14 @@ export class AgentMergeController extends Disposable {
289299
this._runtimes.set(session, runtime);
290300
this._monitoredSessions.add(session);
291301
this._logService.info(`[AgentMergeController] Started session runtime: session=${session}, hasTarget=${agentMerge.target !== undefined}, overrides=${formatOverrideKeys(agentMerge)}`);
302+
if (agentMerge.target) {
303+
const announced = this._announcedConfigurations.get(session);
304+
if (announced) {
305+
this._postConfigurationChangedNotice(session, agentMerge);
306+
} else {
307+
this._announcedConfigurations.set(session, this._getConfiguration(agentMerge));
308+
}
309+
}
292310
}
293311
this._schedule(session, 0);
294312
}
@@ -406,7 +424,7 @@ export class AgentMergeController extends Disposable {
406424
this._logService.info(`[AgentMergeController] Captured session branch and feedback watermark: session=${session}`);
407425
// Announce only on the first capture: a resumed session already has a
408426
// target, so restarting the host must not repeat the notice.
409-
this._postNotice(session, AgentSystemNotificationKind.AgentMergeEnabled, agentMergeEnabledNotice(branchName));
427+
this._postEnabledNotice(session, { ...agentMerge, target });
410428
this._updateAgentMergeState(session, agentMerge, { target });
411429
return;
412430
}
@@ -661,15 +679,50 @@ export class AgentMergeController extends Disposable {
661679
}
662680

663681
private _getConfiguration(agentMerge: AgentMergeSessionState): AgentMergeConfiguration {
664-
const defaults: AgentMergeConfiguration = {
682+
return resolveAgentMergeConfiguration(this._getRootConfiguration(), agentMerge.overrides);
683+
}
684+
685+
private _getRootConfiguration(): AgentMergeConfiguration {
686+
return {
665687
addressReviews: this._configurationService.getRootValue(agentMergeRootConfigSchema, AgentMergeConfigKey.AddressReviews) ?? defaultAgentMergeConfiguration.addressReviews,
666688
fixCI: this._configurationService.getRootValue(agentMergeRootConfigSchema, AgentMergeConfigKey.FixCI) ?? defaultAgentMergeConfiguration.fixCI,
667689
resolveConflicts: this._configurationService.getRootValue(agentMergeRootConfigSchema, AgentMergeConfigKey.ResolveConflicts) ?? defaultAgentMergeConfiguration.resolveConflicts,
668690
mergePullRequest: this._configurationService.getRootValue(agentMergeRootConfigSchema, AgentMergeConfigKey.MergePullRequest) ?? defaultAgentMergeConfiguration.mergePullRequest,
669691
mergeMethod: this._configurationService.getRootValue(agentMergeRootConfigSchema, AgentMergeConfigKey.MergeMethod) ?? defaultAgentMergeConfiguration.mergeMethod,
670692
replyAttribution: this._configurationService.getRootValue(agentMergeRootConfigSchema, AgentMergeConfigKey.ReplyAttribution) ?? defaultAgentMergeConfiguration.replyAttribution,
671693
};
672-
return resolveAgentMergeConfiguration(defaults, agentMerge.overrides);
694+
}
695+
696+
private _postEnabledNotice(session: string, agentMerge: AgentMergeSessionState): void {
697+
if (!agentMerge.enabled
698+
|| !agentMerge.target
699+
|| !this._isFeatureEnabled()
700+
|| this._stateManager.getSessionState(session)?.lifecycle !== SessionLifecycle.Ready) {
701+
return;
702+
}
703+
const configuration = this._getConfiguration(agentMerge);
704+
this._announcedConfigurations.set(session, configuration);
705+
this._postNotice(session, AgentSystemNotificationKind.AgentMergeEnabled, agentMergeEnabledNotice(agentMerge.target, configuration));
706+
}
707+
708+
private _postConfigurationChangedNotice(session: string, current: AgentMergeSessionState | undefined): void {
709+
if (!current?.enabled
710+
|| !current.target
711+
|| !this._isFeatureEnabled()
712+
|| !this._runtimes.has(session)) {
713+
return;
714+
}
715+
const previousConfiguration = this._announcedConfigurations.get(session);
716+
const currentConfiguration = this._getConfiguration(current);
717+
if (!previousConfiguration) {
718+
this._announcedConfigurations.set(session, currentConfiguration);
719+
return;
720+
}
721+
const notice = agentMergeConfigurationChangedNotice(previousConfiguration, currentConfiguration);
722+
this._announcedConfigurations.set(session, currentConfiguration);
723+
if (notice) {
724+
this._postNotice(session, AgentSystemNotificationKind.AgentMergeConfigurationChanged, notice);
725+
}
673726
}
674727

675728
private _canRepairFork(snapshot: PullRequestSnapshot): boolean {
@@ -843,10 +896,12 @@ export class AgentMergeController extends Disposable {
843896
}
844897
this._logService.info(`[AgentMergeController] Turning automatic merge off because a repair turn changed the worktree: session=${session}, repairBaseCommit=${agentMerge.repairBaseCommit}, currentCommit=${currentCommit ?? 'unresolved'}`);
845898
this._postNotice(session, AgentSystemNotificationKind.AgentMergeDisabled, agentMergeMergePullRequestDemotedNotice());
899+
const overrides = { ...agentMerge.overrides, mergePullRequest: 'never' } as const;
900+
this._announcedConfigurations.set(session, this._getConfiguration({ ...agentMerge, overrides }));
846901
this._configurationService.updateSessionConfig(session, {
847902
[SessionConfigKey.AgentMerge]: {
848903
enabled: agentMerge.enabled,
849-
overrides: { ...agentMerge.overrides, mergePullRequest: 'never' },
904+
overrides,
850905
},
851906
// Dropping the baseline is what makes re-selecting the option start
852907
// fresh: without it the next evaluation would demote again against
@@ -897,6 +952,7 @@ export class AgentMergeController extends Disposable {
897952
// Claim the transition before the config write re-enters `_doSyncSession`,
898953
// so the reasoned notice below is the only one the user sees.
899954
this._monitoredSessions.delete(session);
955+
this._announcedConfigurations.delete(session);
900956
this._postNotice(session, AgentSystemNotificationKind.AgentMergeDisabled, reason.notice);
901957
const patch: Record<string, unknown> = {
902958
[SessionConfigKey.AgentMerge]: {

0 commit comments

Comments
 (0)