Skip to content

Commit 4859dc2

Browse files
benibenjCopilot
andcommitted
Merge main into agents/workspace-preselection-telemetry
Preserve upstream archive-nudge and Dev Container changes alongside workspace preselection provenance, acknowledgement, and draft protections. Cover combined Dev Container mode and selection-origin behavior in regression tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2 parents 9125313 + 770a9bc commit 4859dc2

197 files changed

Lines changed: 11512 additions & 1115 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/editor/browser/widget/multiDiffEditor/style.css‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@
138138
&.multiDiffEditor-standard {
139139
.multiDiffEntry {
140140
.collapse-button {
141-
margin: 0 5px;
141+
margin: 0 var(--vscode-spacing-size60);
142142

143143
a {
144144
display: block;
@@ -154,7 +154,7 @@
154154

155155
.header-content {
156156
margin: var(--vscode-spacing-size80) 0 0;
157-
padding: var(--vscode-spacing-size40) 5px;
157+
padding: var(--vscode-spacing-size40) var(--vscode-spacing-size60);
158158
border-top: var(--vscode-strokeThickness) solid var(--vscode-multiDiffEditor-border);
159159
background: var(--vscode-multiDiffEditor-headerBackground);
160160

‎src/vs/platform/actionWidget/browser/actionList.ts‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,15 +1446,14 @@ export class ActionListWidget<T> extends Disposable {
14461446
if ((el.item as { id?: string })?.id === focusedItemId) {
14471447
this._list.setFocus([i]);
14481448
this._list.reveal(i);
1449-
// Move DOM focus back to the list when the list had it: the splice
1450-
// above destroyed the previously focused row, leaving DOM focus on
1451-
// the body.
1452-
if (listHasFocus) {
1453-
this._list.domFocus();
1454-
}
14551449
break;
14561450
}
14571451
}
1452+
if (listHasFocus) {
1453+
// The focused row or its toolbar may have been removed by the update.
1454+
this._focusCheckedOrFirst();
1455+
this._list.domFocus();
1456+
}
14581457
}
14591458
}
14601459
}

‎src/vs/platform/actionWidget/test/browser/actionList.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,6 +1242,26 @@ suite('ActionListWidget', () => {
12421242
);
12431243
});
12441244

1245+
test('removing the focused row toolbar restores focus inside the remaining list', () => {
1246+
const widget = createActionListWidget(disposables, {
1247+
items: [
1248+
{ ...action('one'), toolbarActions: [toAction({ id: 'remove', label: 'Remove', run: () => { } })] },
1249+
action('two'),
1250+
action('three'),
1251+
],
1252+
listOptions: { showFilter: false },
1253+
});
1254+
widget.focus();
1255+
widget.domNode.querySelector<HTMLElement>('.action-list-item-toolbar .action-label')!.focus();
1256+
widget.updateItems([action('two'), action('three')]);
1257+
1258+
assert.deepStrictEqual({
1259+
focusInside: widget.domNode.contains(document.activeElement),
1260+
focusedItem: widget.getFocusedElement()?.item?.id,
1261+
rows: getVisibleRowText(widget),
1262+
}, { focusInside: true, focusedItem: 'two', rows: ['two', 'three'] });
1263+
});
1264+
12451265
test('refreshing the initial selection stays quiet until hover or keyboard navigation', () => runWithFakedTimers({ useFakeTimers: true }, async () => {
12461266
const items = [
12471267
{ ...action('selected'), item: { id: 'selected', checked: true }, hover: { content: 'Selected details' } },

‎src/vs/platform/actionWidget/test/browser/tabbedActionListWidget.test.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,50 @@ suite('TabbedActionListWidget', () => {
346346
widget.hide();
347347
});
348348

349+
for (const focusPanel of [false, true]) {
350+
test(`Escape from a detail ${focusPanel ? 'panel' : 'button'} returns to the list before dismissing the picker`, async () => {
351+
const { widget, contextView } = createWidget(disposables);
352+
const anchor = document.createElement('div');
353+
document.body.appendChild(anchor);
354+
disposables.add({ dispose: () => anchor.remove() });
355+
const button = document.createElement('button');
356+
button.textContent = 'Pin Model';
357+
widget.show<ITestItem>({
358+
user: 'test',
359+
anchor,
360+
tabs: [{ id: 'Models' }],
361+
initialTab: 'Models',
362+
showCheckedItemHover: true,
363+
createActionList: () => ({
364+
items: [{
365+
...action('model'),
366+
item: { id: 'model', checked: true },
367+
hover: { content: button, expandable: true },
368+
}],
369+
listOptions: { persistentHover: true },
370+
}),
371+
delegate: { onSelect: () => { }, onHide: () => { } },
372+
});
373+
const popup = contextView.getContextViewElement();
374+
const panel = popup.querySelector<HTMLElement>('.action-list-submenu-panel')!;
375+
const focusTarget = focusPanel ? panel : button;
376+
focusTarget.focus();
377+
focusTarget.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true, cancelable: true }));
378+
await new Promise<void>(resolve => setTimeout(resolve, 0));
379+
const afterFirstEscape = {
380+
visible: widget.isVisible,
381+
panelHidden: panel.style.display === 'none',
382+
listFocused: popup.querySelector('.monaco-list') === document.activeElement,
383+
};
384+
document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true, cancelable: true }));
385+
386+
assert.deepStrictEqual({ afterFirstEscape, visibleAfterSecondEscape: widget.isVisible }, {
387+
afterFirstEscape: { visible: true, panelHidden: true, listFocused: true },
388+
visibleAfterSecondEscape: false,
389+
});
390+
});
391+
}
392+
349393
for (const motionReduced of [false, true]) {
350394
test(`refresh preserves the focused detail control and moves its row with reduced motion ${motionReduced}`, async () => {
351395
const { widget, contextView } = createWidget(disposables, motionReduced);

‎src/vs/platform/agentHost/AGENTS.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ Agents do **not** maintain the chat catalog, persist membership, know whether a
9595

9696
### Orchestrator layer
9797

98+
Artifact removal uses the VS Code-only `vscode/removeSessionArtifact` extension RPC with `{ session: string, artifactId: string }` and a void result. Clients gate the optional `removeSessionArtifact(URI, string)` connection method with `supportsAgentHostArtifactRemoval(initializeResult)` (`_meta['vscode.removeSessionArtifact'] === true`). This does not extend the generated AHP protocol.
99+
100+
The shared `node/shared/sessionArtifacts.ts` path serializes artifact mutations per session across tools and direct user requests. Each mutation reads the latest collection, awaits the existing `sessionArtifacts` database metadata write, then publishes `SessionMetaChanged` merged with the latest independent metadata. Failed deletion leaves the artifact visible and retryable; failures are logged and propagated without blocking queued additions. Independent GitHub associations and unrelated artifacts/references are preserved. No model turn or tool invocation is involved in direct user removal.
101+
98102
**`AgentService` (`node/agentService.ts`):**
99103
- Resolves the `(session, chat)` → `(agent, session URI, chat URI)` mapping for orchestration.
100104
- Uses `IAgentHostProviderService` for provider ownership and session routing. Its `getProviderForSession` path falls back through the session URI's scheme when a restored session was not associated in this process lifetime.

‎src/vs/platform/agentHost/browser/agentHostProtocolClient.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { FileSystemProviderErrorCode, toFileSystemProviderErrorCode } from '../.
1919
import { ConfigurationTarget, ConfigurationTargetToString, IConfigurationService } from '../../configuration/common/configuration.js';
2020
import { AgentSession, IAgentCreateChatRequestOptions, IAgentCreateSessionConfig, IAgentResolveSessionConfigParams, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, AuthenticateParams, AuthenticateResult, IMcpNotification } from '../common/agent.js';
2121
import { AGENT_HOST_DEBUG_LOGS_CHUNK_BYTES, AGENT_HOST_DEBUG_LOGS_MAX_ENTRIES, IAgentConnection, IAgentHostManagedSettingsDiagnostics, IAgentHostNetworkDiagnosticsInfo, IAgentHostNetworkFetchResult, type AgentHostDebugLogsArtifactKind, type IAgentHostDebugLogsArtifact, type IAgentHostDebugLogsChunk } from '../common/agentService.js';
22-
import { ClaimAgentHostDetachedWorktreeExtensionMethod, CollectAgentHostDebugLogsExtensionMethod, CreateAgentHostDetachedWorktreeExtensionMethod, DeleteAgentHostDetachedWorktreeExtensionMethod, GetAgentHostSessionStateFileExtensionMethod, ReadAgentHostDebugLogsChunkExtensionMethod, ReconcileAgentHostDetachedWorktreesExtensionMethod, RequestAgentHostWorkspaceTrustExtensionMethod, SetAgentHostDetachedWorktreeArchivedExtensionMethod, supportsAgentHostChatStateFile, type IAgentHostExtensionCommandMap, type IAgentHostExtensionInitializeResult, type IAgentHostExtensionServerCommandMap } from '../common/agentHostExtensionProtocol.js';
22+
import { ClaimAgentHostDetachedWorktreeExtensionMethod, CollectAgentHostDebugLogsExtensionMethod, CreateAgentHostDetachedWorktreeExtensionMethod, DeleteAgentHostDetachedWorktreeExtensionMethod, GetAgentHostSessionStateFileExtensionMethod, ReadAgentHostDebugLogsChunkExtensionMethod, ReconcileAgentHostDetachedWorktreesExtensionMethod, RemoveSessionArtifactExtensionMethod, RequestAgentHostWorkspaceTrustExtensionMethod, SetAgentHostDetachedWorktreeArchivedExtensionMethod, supportsAgentHostChatStateFile, type IAgentHostExtensionCommandMap, type IAgentHostExtensionInitializeResult, type IAgentHostExtensionServerCommandMap } from '../common/agentHostExtensionProtocol.js';
2323
import { AMBIENT_AGENT_HOST_AUTHORITY } from '../common/agentHostConnectionsService.js';
2424
import { createRemoteWatchHandle, type IRemoteWatchHandle } from '../common/agentHostFileSystemProvider.js';
2525
import { AgentSubscriptionManager, type IActiveSubscriptionInfo, type IAgentSubscription } from '../common/state/agentSubscription.js';
@@ -1320,6 +1320,10 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
13201320
return promise;
13211321
}
13221322

1323+
async removeSessionArtifact(session: URI, artifactId: string): Promise<void> {
1324+
await this._sendExtensionRequest(RemoveSessionArtifactExtensionMethod, { session: session.toString(), artifactId });
1325+
}
1326+
13231327
async createDetachedWorktree(session: URI, prompt: string): Promise<{ handle: string; worktree: URI }> {
13241328
const result = await this._sendExtensionRequest(CreateAgentHostDetachedWorktreeExtensionMethod, {
13251329
session: session.toString(),

‎src/vs/platform/agentHost/browser/nullAgentHostService.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export class NullAgentHostService implements IAgentHostService {
6161
async readDebugLogsChunk(_resource: URI, _position: number): Promise<IAgentHostDebugLogsChunk> { return notSupported(); }
6262
async listSessions(): Promise<IAgentSessionMetadata[]> { return []; }
6363
async createSession(_config?: IAgentCreateSessionConfig): Promise<URI> { return notSupported(); }
64+
async removeSessionArtifact(_session: URI, _artifactId: string): Promise<void> { return notSupported(); }
6465
async createDetachedWorktree(_session: URI, _prompt: string): Promise<{ handle: string; worktree: URI }> { return notSupported(); }
6566
async claimDetachedWorktree(_handle: string): Promise<void> { return notSupported(); }
6667
async setDetachedWorktreeArchived(_handle: string, _archived: boolean): Promise<void> { return notSupported(); }

‎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

0 commit comments

Comments
 (0)