Skip to content

Commit a2f72da

Browse files
authored
Merge branch 'main' into agents/codex-cross-app-continuation
2 parents cccc9c7 + 61b8b27 commit a2f72da

95 files changed

Lines changed: 7252 additions & 640 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,7 @@ export interface IAgentModelInfo {
860860
export type AgentSignal =
861861
| IAgentActionSignal
862862
| IAgentModelCallCompletedSignal
863+
| IAgentModelCallFinishedSignal
863864
| IAgentToolPendingConfirmationSignal
864865
| IAgentSubagentStartedSignal
865866
| IAgentSubagentResumedSignal
@@ -897,6 +898,27 @@ export interface IAgentModelCallCompletedSignal {
897898
readonly parentToolCallId?: string;
898899
}
899900

901+
export type AgentModelCallFinishedOutcome = 'success' | 'error' | 'cancelled' | 'rejected';
902+
903+
/** Reports the final lifecycle outcome of one dispatched model-call attempt. */
904+
export interface IAgentModelCallFinishedSignal {
905+
readonly kind: 'model_call_finished';
906+
/** Target chat channel URI. For inner subagent calls this is the parent chat channel. */
907+
readonly resource: URI;
908+
/** Host turn identifier owning this model-call attempt. */
909+
readonly turnId: string;
910+
/** Stable SDK event identifier used to suppress duplicate notifications. */
911+
readonly modelCallId: string;
912+
/** Monotonic provider-dispatch duration in milliseconds. */
913+
readonly dispatchDurationMs: number;
914+
readonly outcome: AgentModelCallFinishedOutcome;
915+
/** Present only for accepted successful responses. */
916+
readonly containsBuiltInFileEditRequest?: boolean;
917+
readonly editClassifierVersion: number;
918+
/** If set, route the model call to the subagent session belonging to this tool call. */
919+
readonly parentToolCallId?: string;
920+
}
921+
900922
/**
901923
* A tool has finished collecting parameters and needs the host to decide
902924
* whether it should run (or, mid-execution, re-confirm). The host applies

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,8 @@ export interface IAgentHostChangesetOperationService extends IDisposable {
143143
*/
144144
registerContribution(contribution: IChangesetOperationContribution): IDisposable;
145145
/**
146-
* Recomputes and publishes operations for the changesets for a given
147-
* session. If `gitState` is not provided, the current git state will
148-
* be used.
146+
* Recomputes operations using the provided or current Git state.
147+
* Without Git state, clears cached operations but defers initial publication.
149148
*/
150149
updateOperations(sessionKey: string, changeset?: string, gitState?: ISessionGitState, gitHubState?: ISessionGitHubState): void;
151150

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const META_CHANGESET_SESSION = 'agentHost.changeset.session';
2020
*/
2121
export const META_LEGACY_DIFFS = 'diffs';
2222

23-
/** Cached aggregate from Session Changes for every session. */
23+
/** Cached aggregate from Branch Changes for worktree sessions, otherwise Session Changes. */
2424
export const META_CHANGES_SUMMARY = 'agentHost.changes';
2525

2626
/**
@@ -39,10 +39,8 @@ export const CHANGESET_DB_METADATA_KEYS: Record<string, true> = {
3939
};
4040

4141
/**
42-
* The minimal key set that carries only the small persisted
43-
* {@link META_CHANGES_SUMMARY} aggregate (no large diff blobs). Requested when a
44-
* session changeset is ready, so the caller can preserve previously cached
45-
* counts without loading the diff blobs.
42+
* Reads the small persisted aggregate without diff blobs.
43+
* Used when both possible summary changesets are ready.
4644
*/
4745
export const CHANGES_SUMMARY_METADATA_KEYS: Record<string, true> = {
4846
[META_CHANGES_SUMMARY]: true,
@@ -176,8 +174,8 @@ export interface IAgentHostChangesetService {
176174
* aggregate should be advertised (loaded session whose `summary.changes`
177175
* the caller already projected, or no live/persisted source).
178176
*
179-
* Prefers live or persisted summary counts, falling back to Session Changes diffs.
180-
* Existing caches are refreshed from Session Changes when opened.
177+
* Prefers live or persisted summary counts, falling back to the isolation-selected changeset.
178+
* Existing caches are refreshed from that changeset when opened.
181179
*/
182180
computeListEntryChanges(sessionUri: ProtocolURI, metadata: Record<string, string | undefined>): ChangesSummary | undefined;
183181

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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 { isObject } from '../../../../base/common/types.js';
7+
import { localize } from '../../../../nls.js';
8+
import { AgentMergeActions, isAgentMergeMergePullRequest } from '../agentMerge.js';
9+
import type { InvokeChangesetOperationResult } from '../state/protocol/channels-changeset/commands.js';
10+
import { JsonRpcErrorCodes, ProtocolError } from '../state/sessionProtocol.js';
11+
12+
export const PREPARE_PULL_REQUEST_OPERATION_ID = 'prepare-pull-request';
13+
14+
const PULL_REQUEST_META_KEY = 'vscode.pullRequest';
15+
const DETAILS_DATA_URI_PREFIX = 'data:application/json,';
16+
17+
export interface IPullRequestContext {
18+
readonly workingDirectory: string;
19+
readonly repository: string;
20+
readonly branchName: string;
21+
readonly baseBranchName: string;
22+
readonly headOwner?: string;
23+
readonly upstreamBranchName?: string;
24+
}
25+
26+
export interface IPullRequestCreateOptions {
27+
readonly title: string;
28+
readonly description: string;
29+
readonly draft: boolean;
30+
readonly agentMerge: boolean;
31+
readonly agentMergeOptions?: AgentMergeActions;
32+
readonly autoMergeMethod?: 'MERGE' | 'SQUASH' | 'REBASE';
33+
readonly expectedContext?: IPullRequestContext;
34+
}
35+
36+
export interface IPullRequestDetails {
37+
readonly title: string;
38+
readonly description: string;
39+
readonly branchName: string;
40+
readonly baseBranchName: string;
41+
readonly repository: string;
42+
readonly autoMergeAllowed: boolean;
43+
readonly mergeMethods: readonly ('MERGE' | 'SQUASH' | 'REBASE')[];
44+
readonly agentMergeAvailable: boolean;
45+
readonly agentMergeOptions?: AgentMergeActions;
46+
readonly generationError?: string;
47+
readonly context?: IPullRequestContext;
48+
}
49+
50+
interface IHasPullRequestOperationMeta {
51+
readonly _meta?: Record<string, unknown>;
52+
}
53+
54+
function isMergeMethod(value: unknown): value is NonNullable<IPullRequestCreateOptions['autoMergeMethod']> {
55+
return value === 'MERGE' || value === 'SQUASH' || value === 'REBASE';
56+
}
57+
58+
function isRecord(value: unknown): value is Record<string, unknown> {
59+
return isObject(value);
60+
}
61+
62+
function parseContext(value: unknown): IPullRequestContext {
63+
if (!isRecord(value)
64+
|| typeof value.workingDirectory !== 'string' || !value.workingDirectory.trim()
65+
|| typeof value.repository !== 'string' || !value.repository.trim()
66+
|| typeof value.branchName !== 'string' || !value.branchName.trim()
67+
|| typeof value.baseBranchName !== 'string' || !value.baseBranchName.trim()
68+
|| (value.headOwner !== undefined && (typeof value.headOwner !== 'string' || !value.headOwner.trim()))
69+
|| (value.upstreamBranchName !== undefined && (typeof value.upstreamBranchName !== 'string' || !value.upstreamBranchName.trim()))) {
70+
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('agentHost.pr.invalidContext', "Invalid pull request preparation context."));
71+
}
72+
return {
73+
workingDirectory: value.workingDirectory,
74+
repository: value.repository,
75+
branchName: value.branchName,
76+
baseBranchName: value.baseBranchName,
77+
...(value.headOwner !== undefined ? { headOwner: value.headOwner } : {}),
78+
...(value.upstreamBranchName !== undefined ? { upstreamBranchName: value.upstreamBranchName } : {}),
79+
};
80+
}
81+
82+
function parseAgentMergeOptions(value: unknown): AgentMergeActions {
83+
if (!isRecord(value) || typeof value.addressReviews !== 'boolean' || typeof value.fixCI !== 'boolean'
84+
|| typeof value.resolveConflicts !== 'boolean' || !isAgentMergeMergePullRequest(value.mergePullRequest)) {
85+
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('agentHost.pr.invalidAgentMergeOptions', "Invalid Agent Merge configuration."));
86+
}
87+
return {
88+
addressReviews: value.addressReviews,
89+
fixCI: value.fixCI,
90+
resolveConflicts: value.resolveConflicts,
91+
mergePullRequest: value.mergePullRequest,
92+
};
93+
}
94+
95+
function parseCreateOptions(value: unknown): IPullRequestCreateOptions {
96+
if (!isRecord(value)
97+
|| typeof value.title !== 'string' || typeof value.description !== 'string'
98+
|| typeof value.draft !== 'boolean' || typeof value.agentMerge !== 'boolean') {
99+
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('agentHost.pr.invalidOptions', "Invalid pull request creation options."));
100+
}
101+
const autoMergeMethod = value.autoMergeMethod;
102+
if (autoMergeMethod !== undefined && !isMergeMethod(autoMergeMethod)) {
103+
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('agentHost.pr.invalidMergeMethod', "Invalid pull request auto-merge method."));
104+
}
105+
if (!value.title.trim()) {
106+
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('agentHost.pr.titleRequired', "A pull request title is required."));
107+
}
108+
if (value.draft && autoMergeMethod) {
109+
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('agentHost.pr.draftAutoMerge', "Draft pull requests cannot use GitHub auto-merge."));
110+
}
111+
if (value.agentMerge && autoMergeMethod) {
112+
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('agentHost.pr.conflictingMergeOptions', "Agent Merge and GitHub auto-merge cannot be enabled together."));
113+
}
114+
if (value.agentMergeOptions !== undefined && !value.agentMerge) {
115+
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('agentHost.pr.agentMergeOptionsWithoutEnablement', "Enable Agent Merge to configure it for this pull request."));
116+
}
117+
return {
118+
title: value.title,
119+
description: value.description,
120+
draft: value.draft,
121+
agentMerge: value.agentMerge,
122+
...(value.agentMergeOptions !== undefined ? { agentMergeOptions: parseAgentMergeOptions(value.agentMergeOptions) } : {}),
123+
...(autoMergeMethod ? { autoMergeMethod } : {}),
124+
...(value.expectedContext !== undefined ? { expectedContext: parseContext(value.expectedContext) } : {}),
125+
};
126+
}
127+
128+
export function createPullRequestValidationMeta(context: IPullRequestContext): Record<string, unknown> {
129+
return { [PULL_REQUEST_META_KEY]: { validateOnly: true, expectedContext: parseContext(context) } };
130+
}
131+
132+
export function readPullRequestValidationMeta(source: IHasPullRequestOperationMeta): IPullRequestContext | undefined {
133+
if (source._meta === undefined) {
134+
return undefined;
135+
}
136+
if (!isObject(source._meta)) {
137+
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('agentHost.pr.invalidMeta', "Invalid pull request operation metadata."));
138+
}
139+
if (!Object.hasOwn(source._meta, PULL_REQUEST_META_KEY)) {
140+
return undefined;
141+
}
142+
const value = source._meta[PULL_REQUEST_META_KEY];
143+
if (!isRecord(value) || value.validateOnly !== true) {
144+
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('agentHost.pr.invalidValidation', "Invalid pull request context validation request."));
145+
}
146+
return parseContext(value.expectedContext);
147+
}
148+
149+
export function createPullRequestOperationMeta(options: IPullRequestCreateOptions): Record<string, unknown> {
150+
return { [PULL_REQUEST_META_KEY]: parseCreateOptions(options) };
151+
}
152+
153+
/** Missing options preserve legacy creation behavior; malformed options are rejected. */
154+
export function readPullRequestOperationMeta(source: IHasPullRequestOperationMeta): IPullRequestCreateOptions | undefined {
155+
const meta = source._meta;
156+
if (meta === undefined) {
157+
return undefined;
158+
}
159+
if (!isObject(meta)) {
160+
throw new ProtocolError(JsonRpcErrorCodes.InvalidParams, localize('agentHost.pr.invalidMeta', "Invalid pull request operation metadata."));
161+
}
162+
return Object.hasOwn(meta, PULL_REQUEST_META_KEY) ? parseCreateOptions(meta[PULL_REQUEST_META_KEY]) : undefined;
163+
}
164+
165+
function invalidDetails(): Error {
166+
return new Error(localize('agentHost.pr.invalidDetails', "Invalid pull request preparation result."));
167+
}
168+
169+
function parseDetails(value: unknown): IPullRequestDetails {
170+
if (!isRecord(value) || typeof value.title !== 'string' || typeof value.description !== 'string'
171+
|| typeof value.branchName !== 'string' || !value.branchName.trim()
172+
|| typeof value.baseBranchName !== 'string' || !value.baseBranchName.trim()
173+
|| typeof value.repository !== 'string' || !value.repository.trim()
174+
|| typeof value.autoMergeAllowed !== 'boolean' || typeof value.agentMergeAvailable !== 'boolean'
175+
|| !Array.isArray(value.mergeMethods) || !value.mergeMethods.every(isMergeMethod)) {
176+
throw invalidDetails();
177+
}
178+
const generationError = value.generationError;
179+
if (generationError !== undefined && typeof generationError !== 'string') {
180+
throw invalidDetails();
181+
}
182+
return {
183+
title: value.title,
184+
description: value.description,
185+
branchName: value.branchName,
186+
baseBranchName: value.baseBranchName,
187+
repository: value.repository,
188+
autoMergeAllowed: value.autoMergeAllowed,
189+
mergeMethods: [...value.mergeMethods],
190+
agentMergeAvailable: value.agentMergeAvailable,
191+
...(value.agentMergeOptions !== undefined ? { agentMergeOptions: parseAgentMergeOptions(value.agentMergeOptions) } : {}),
192+
...(generationError !== undefined ? { generationError } : {}),
193+
...(value.context !== undefined ? { context: parseContext(value.context) } : {}),
194+
};
195+
}
196+
197+
/** Serializes an immutable preparation snapshot using the protocol's content reference. */
198+
export function createPullRequestDetailsResult(details: IPullRequestDetails): InvokeChangesetOperationResult {
199+
return {
200+
followUp: {
201+
content: {
202+
uri: `${DETAILS_DATA_URI_PREFIX}${encodeURIComponent(JSON.stringify(parseDetails(details)))}`,
203+
contentType: 'application/json',
204+
},
205+
},
206+
};
207+
}
208+
209+
export function readPullRequestDetailsResult(result: InvokeChangesetOperationResult): IPullRequestDetails {
210+
const followUp = result.followUp;
211+
const content = followUp?.content;
212+
if ((followUp?.external !== undefined && followUp.external !== false)
213+
|| content?.contentType !== 'application/json' || typeof content.uri !== 'string'
214+
|| !content.uri.startsWith(DETAILS_DATA_URI_PREFIX)) {
215+
throw invalidDetails();
216+
}
217+
const encoded = content.uri.slice(DETAILS_DATA_URI_PREFIX.length);
218+
if (!/^(?:[a-zA-Z0-9_.!~*'()-]|%[a-fA-F0-9]{2})+$/.test(encoded)) {
219+
throw invalidDetails();
220+
}
221+
let details: unknown;
222+
try {
223+
details = JSON.parse(decodeURIComponent(encoded));
224+
} catch {
225+
throw invalidDetails();
226+
}
227+
return parseDetails(details);
228+
}

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { Disposable } from '../../../base/common/lifecycle.js';
77
import { URI } from '../../../base/common/uri.js';
88
import { IAgentSessionMetadata } from '../common/agent.js';
9-
import { ChangesetKind, parseChangesetUri } from '../common/changesetUri.js';
9+
import { buildBranchChangesetUri, buildSessionChangesetUri, ChangesetKind, parseChangesetUri } from '../common/changesetUri.js';
1010
import { ChangesetFileMonitorCoordinator } from './agentHostChangesetFileMonitorCoordinator.js';
1111
import { AgentHostStateManager, IAgentHostStateManager } from './agentHostStateManager.js';
1212
import { IAgentHostChangesetService, META_CHANGESET_BRANCH, META_CHANGESET_SESSION, META_LEGACY_DIFFS } from '../common/agentHostChangesetService.js';
@@ -16,6 +16,7 @@ import { IAgentHostGitStateService } from '../common/agentHostGitStateService.js
1616
import { IInstantiationService } from '../../instantiation/common/instantiation.js';
1717
import { readAgentMergeSessionState } from '../common/agentMerge.js';
1818
import { isAhpChatChannel, parseSubagentSessionUri, type SessionConfigState } from '../common/state/sessionState.js';
19+
import { getSummaryChangesetKind } from './agentHostChangesetSummary.js';
1920

2021
/**
2122
* Raw metadata blob values for the session DB, batch-read by the caller.
@@ -93,9 +94,10 @@ export class AgentHostChangesetCoordinator extends Disposable {
9394
this._changesetFileMonitor.onSessionRestored(sessionStr);
9495
}
9596

96-
/** Refreshes config-dependent catalogue entries after restored session config is seeded. */
97-
onSessionConfigRestored(sessionStr: string): void {
97+
/** Refreshes the catalogue and summary interest after replacing the previous config during restore. */
98+
onSessionConfigRestored(sessionStr: string, previous: SessionConfigState | undefined): void {
9899
this._changesets.refreshChangesetCatalog(sessionStr);
100+
this._refreshSummarySource(sessionStr, previous);
99101
}
100102

101103
/**
@@ -125,13 +127,23 @@ export class AgentHostChangesetCoordinator extends Disposable {
125127
}
126128

127129
private onDidChangeSessionConfig(session: string, previous: SessionConfigState | undefined, current: SessionConfigState | undefined): void {
130+
this._refreshSummarySource(session, previous);
128131
const wasEnabled = readAgentMergeSessionState(previous?.values)?.enabled === true;
129132
const isEnabled = readAgentMergeSessionState(current?.values)?.enabled === true;
130133
if (wasEnabled !== isEnabled) {
131134
this._changesets.refreshChangesetCatalog(session);
132135
}
133136
}
134137

138+
private _refreshSummarySource(session: string, previous: SessionConfigState | undefined): void {
139+
const kind = getSummaryChangesetKind(this._stateManager.getSessionState(session)?.config?.values);
140+
if (kind !== getSummaryChangesetKind(previous?.values)) {
141+
this._changesetOperationService.updateOperations(session, buildBranchChangesetUri(session));
142+
this._changesetOperationService.updateOperations(session, buildSessionChangesetUri(session));
143+
this._changesets.recomputeSubscribedChangesets(session);
144+
}
145+
}
146+
135147
// ---- Subscription hooks -------------------------------------------------
136148

137149
/**
@@ -195,7 +207,12 @@ export class AgentHostChangesetCoordinator extends Disposable {
195207
}
196208

197209
this._addSubscription(session, session);
198-
this._changesets.refreshSessionChangeset(session);
210+
const kind = getSummaryChangesetKind(this._stateManager.getSessionState(session)?.config?.values);
211+
if (kind === ChangesetKind.Branch) {
212+
this._changesets.refreshBranchChangeset(session);
213+
} else {
214+
this._changesets.refreshSessionChangeset(session);
215+
}
199216
this._changesetFileMonitor.trackSessionChanges(session, session);
200217
}
201218

0 commit comments

Comments
 (0)