Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f3b0544
agentHost: 9728-style protected resource auth
connor4312 Mar 18, 2026
e3547da
cleanup old setAuthToken
connor4312 Mar 18, 2026
c5fc3c0
tests
connor4312 Mar 18, 2026
da0cdcf
Merge remote-tracking branch 'origin/main' into connor4312/ahp-auth
connor4312 Mar 18, 2026
3af163a
comments
connor4312 Mar 18, 2026
8712752
Fix infinite enter for windows terminal 5 when screen reader is enabled
anthonykim1 Mar 19, 2026
b7ba4a7
Add model picker telemetry for admin/upgrade links and Other Models t…
sandy081 Mar 19, 2026
970e6fb
sessions - allow to collapse section headers (#303228)
bpasero Mar 19, 2026
000a053
Sessions - refactor changes view (#303256)
lszomoru Mar 19, 2026
08316e4
sessions - fix title bar issue on macOS (#303224)
bpasero Mar 19, 2026
58757c0
Use bracketed paste for multiline executed terminal text (#302526)
jcansdale Mar 19, 2026
3b60cf4
chore - Add telemetry logging for chat editing session store and rest…
jrieken Mar 19, 2026
8b06f20
fix populating copilot featured models (#303262)
sandy081 Mar 19, 2026
b369f04
Enhance URL glob matching to enforce subdomain wildcard matching on d…
TylerLeonhardt Mar 19, 2026
5e3ed39
chat - prevent race conditions in `loadSession` (#303244)
bpasero Mar 19, 2026
44e1de5
component explorer fixture for chat customization tabs (#303243)
aeschli Mar 19, 2026
7fbc59c
Sessions: Consider making `Group by Repository` the default state (fi…
bpasero Mar 19, 2026
5c7192e
sessions - tweaks to grouping by repo (fix #302453) (#303250)
bpasero Mar 19, 2026
9ddc847
Add recent repository label tracking to agent sessions control
osortega Mar 19, 2026
444471e
Merge branch 'main' into anthonykim1/fixPsreadlineRegression
anthonykim1 Mar 19, 2026
bf5f2dc
docs - update copilot instructions for event usage (#303252)
bpasero Mar 19, 2026
bb5aae3
plugins: fix a bunch of issues in customizations (#303270)
connor4312 Mar 19, 2026
21f384a
sessions - allow to create new chat per repository from the section h…
bpasero Mar 19, 2026
1d22de6
Bug fix: Fix skill load regression (#303277)
vijayupadya Mar 19, 2026
f533b7f
Add command to sessionStart hook for environment setup
osortega Mar 19, 2026
6e24743
Add resetSectionCollapseState method to AgentSessionsControl and invo…
osortega Mar 19, 2026
6a8fe54
Call resetSectionCollapseState on sessionsControl after updating grou…
osortega Mar 19, 2026
9b8ad50
Clean up
osortega Mar 19, 2026
d78a103
sessions - action renames (#303291)
bpasero Mar 19, 2026
3cf5a7e
Include "Other" repo label in recent repository tracking
osortega Mar 19, 2026
58c1dd9
Centralize "Other" label and ensure ordering in repo grouping
osortega Mar 19, 2026
ee0567d
Merge pull request #303242 from microsoft/anthonykim1/fixPsreadlineRe…
mjbvz Mar 19, 2026
e2172fd
Merge origin/main and resolve conflicts
osortega Mar 19, 2026
750a39f
sessions - viewer tweaks (#303298)
bpasero Mar 19, 2026
38742b5
Browser Quick Open / Tab Management (#303058)
kycutler Mar 19, 2026
fe5c4e1
Merge pull request #302995 from microsoft/connor4312/ahp-auth
connor4312 Mar 19, 2026
c5e5efa
Merge pull request #303299 from microsoft/copilot/inherent-hornet
osortega Mar 19, 2026
3f4f33b
Fix close tracking in browser API (#303304)
kycutler Mar 19, 2026
55a8db1
Sessions - more changes view cleanup (#303303)
lszomoru Mar 19, 2026
caef4ed
fix aiCustomizationListWidget.fixture.ts (#303311)
aeschli Mar 19, 2026
b4018e3
Never use simple browser on desktop (#303312)
kycutler Mar 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ function f(x: number, y: string): void { }
- You MUST NOT use storage keys of another component only to make changes to that component. You MUST come up with proper API to change another component.
- Use `IEditorService` to open editors instead of `IEditorGroupsService.activeGroup.openEditor` to ensure that the editor opening logic is properly followed and to avoid bypassing important features such as `revealIfOpened` or `preserveFocus`.
- Avoid using `bind()`, `call()` and `apply()` solely to control `this` or partially apply arguments; prefer arrow functions or closures to capture the necessary context, and use these methods only when required by an API or interoperability.
- Avoid using events to drive control flow between components. Instead, prefer direct method calls or service interactions to ensure clearer dependencies and easier traceability of logic. Events should be reserved for broadcasting state changes or notifications rather than orchestrating behavior across components.

## Learnings
- Minimize the amount of assertions in tests. Prefer one snapshot-style `assert.deepStrictEqual` over multiple precise assertions, as they are much more difficult to understand and to update.
14 changes: 8 additions & 6 deletions extensions/simple-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@
"category": "Simple Browser"
}
],
"menus": {
"commandPalette": [
{
"command": "simpleBrowser.show",
"when": "isWeb"
}
]
},
"configuration": [
{
"title": "Simple Browser",
Expand All @@ -51,12 +59,6 @@
"default": true,
"title": "Focus Lock Indicator Enabled",
"description": "%configuration.focusLockIndicator.enabled.description%"
},
"simpleBrowser.useIntegratedBrowser": {
"type": "boolean",
"default": true,
"markdownDescription": "%configuration.useIntegratedBrowser.description%",
"scope": "application"
}
}
}
Expand Down
3 changes: 1 addition & 2 deletions extensions/simple-browser/package.nls.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"displayName": "Simple Browser",
"description": "A very basic built-in webview for displaying web content.",
"configuration.focusLockIndicator.enabled.description": "Enable/disable the floating indicator that shows when focused in the simple browser.",
"configuration.useIntegratedBrowser.description": "When enabled, the `simpleBrowser.show` command will open URLs in the integrated browser instead of the Simple Browser webview. **Note:** This setting is only available on desktop."
"configuration.focusLockIndicator.enabled.description": "Enable/disable the floating indicator that shows when focused in the simple browser."
}
7 changes: 0 additions & 7 deletions extensions/simple-browser/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ declare class URL {
const openApiCommand = 'simpleBrowser.api.open';
const showCommand = 'simpleBrowser.show';
const integratedBrowserCommand = 'workbench.action.browser.open';
const useIntegratedBrowserSetting = 'simpleBrowser.useIntegratedBrowser';

const enabledHosts = new Set<string>([
'localhost',
Expand All @@ -37,12 +36,6 @@ const openerId = 'simpleBrowser.open';
* Checks if the integrated browser should be used instead of the simple browser
*/
async function shouldUseIntegratedBrowser(): Promise<boolean> {
const config = vscode.workspace.getConfiguration();
if (!config.get<boolean>(useIntegratedBrowserSetting, true)) {
return false;
}

// Verify that the integrated browser command is available
const commands = await vscode.commands.getCommands(true);
return commands.includes(integratedBrowserCommand);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import * as assert from 'assert';
import * as vscode from 'vscode';
import { window, ViewColumn } from 'vscode';
import { window, commands, ViewColumn } from 'vscode';
import { assertNoRpc, closeAllEditors } from '../utils';

(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('vscode API - browser', () => {
Expand Down Expand Up @@ -73,6 +73,16 @@ import { assertNoRpc, closeAllEditors } from '../utils';
assert.strictEqual(window.browserTabs.length, countBefore - 1);
});

test('Can move a browser tab to a new group and close it successfully', async () => {
const tab = await window.openBrowserTab('about:blank');
assert.ok(window.browserTabs.includes(tab));

await commands.executeCommand('workbench.action.moveEditorToNextGroup');

await tab.close();
assert.ok(!window.browserTabs.includes(tab));
});

// #endregion

// #region onDidOpenBrowserTab
Expand Down
26 changes: 24 additions & 2 deletions src/vs/platform/actionWidget/browser/actionList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ class ActionItemRenderer<T> implements IListRenderer<IActionListItem<T>, IAction
private readonly _onRemoveItem: ((item: IActionListItem<T>) => void) | undefined,
private readonly _onSubmenuIndicatorHover: ((element: IActionListItem<T>, indicator: HTMLElement, disposables: DisposableStore) => void) | undefined,
private _hasAnySubmenuActions: boolean,
private readonly _linkHandler: ((uri: URI, item: IActionListItem<T>) => void) | undefined,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@IOpenerService private readonly _openerService: IOpenerService,
) { }
Expand Down Expand Up @@ -284,7 +285,12 @@ class ActionItemRenderer<T> implements IListRenderer<IActionListItem<T>, IAction
} else {
const rendered = renderMarkdown(element.description, {
actionHandler: (content: string) => {
this._openerService.open(URI.parse(content), { allowCommands: true });
const uri = URI.parse(content);
if (this._linkHandler) {
this._linkHandler(uri, element);
} else {
void this._openerService.open(uri, { allowCommands: true });
}
}
});
data.elementDisposables.add(rendered);
Expand Down Expand Up @@ -404,6 +410,17 @@ export interface IActionListOptions {
*/
readonly minWidth?: number;

/**
* Optional handler for markdown links activated in item descriptions or hovers.
* When unset, links open via the opener service with command links allowed.
*/
readonly linkHandler?: (uri: URI, item: IActionListItem<unknown>) => void;

/**
* Optional callback fired when a section's collapsed state changes.
*/
readonly onDidToggleSection?: (section: string, collapsed: boolean) => void;

/**
* When true, descriptions are rendered as subtext below the title
* instead of inline to the right.
Expand Down Expand Up @@ -518,7 +535,7 @@ export class ActionListWidget<T> extends Disposable {
const hasAnySubmenuActions = items.some(item => !!item.submenuActions?.length);

this._list = this._register(new List(user, this.domNode, virtualDelegate, [
new ActionItemRenderer<T>(preview, (item) => this._removeItem(item), (element, indicator, disposables) => this._wireSubmenuIndicator(element, indicator, disposables), hasAnySubmenuActions, this._keybindingService, this._openerService),
new ActionItemRenderer<T>(preview, (item) => this._removeItem(item), (element, indicator, disposables) => this._wireSubmenuIndicator(element, indicator, disposables), hasAnySubmenuActions, this._options?.linkHandler, this._keybindingService, this._openerService),
new HeaderRenderer(),
new SeparatorRenderer(),
], {
Expand Down Expand Up @@ -638,6 +655,7 @@ export class ActionListWidget<T> extends Disposable {
} else {
this._collapsedSections.add(section);
}
this._options?.onDidToggleSection?.(section, this._collapsedSections.has(section));
this._applyFilter();
}

Expand Down Expand Up @@ -1162,10 +1180,14 @@ export class ActionListWidget<T> extends Disposable {
}

const markdown = typeof element.hover!.content === 'string' ? new MarkdownString(element.hover!.content) : element.hover!.content;
const linkHandler = this._options?.linkHandler;
this._hover.value = this._hoverService.showDelayedHover({
content: markdown ?? '',
target: rowElement,
additionalClasses: ['action-widget-hover'],
linkHandler: linkHandler ? (url: string) => {
linkHandler(URI.parse(url), element);
} : undefined,
position: {
hoverPosition: HoverPosition.LEFT,
forcePosition: false,
Expand Down
72 changes: 67 additions & 5 deletions src/vs/platform/agentHost/common/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { Event } from '../../../base/common/event.js';
import { IAuthorizationProtectedResourceMetadata } from '../../../base/common/oauth.js';
import { URI } from '../../../base/common/uri.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import type { IActionEnvelope, INotification, ISessionAction } from './state/sessionActions.js';
Expand Down Expand Up @@ -40,10 +41,55 @@ export interface IAgentDescriptor {
readonly provider: AgentProvider;
readonly displayName: string;
readonly description: string;
/** Whether the renderer should push a GitHub auth token for this agent. */
/**
* Whether the renderer should push a GitHub auth token for this agent.
* @deprecated Use {@link IResourceMetadata.resources} from {@link IAgentService.getResourceMetadata} instead.
*/
readonly requiresAuth: boolean;
}

// ---- Auth types (RFC 9728 / RFC 6750 inspired) -----------------------------

/**
* Describes the agent host as an OAuth 2.0 protected resource.
* Uses {@link IAuthorizationProtectedResourceMetadata} from RFC 9728
* to describe auth requirements, enabling clients to resolve tokens
* using the standard VS Code authentication service.
*
* Returned from the server via {@link IAgentService.getResourceMetadata}.
*/
export interface IResourceMetadata {
/**
* Protected resources the agent host requires authentication for.
* Each entry uses the standard RFC 9728 shape so clients can resolve
* tokens via {@link IAuthenticationService.getOrActivateProviderIdForServer}.
*/
readonly resources: readonly IAuthorizationProtectedResourceMetadata[];
}

/**
* Parameters for the `authenticate` command.
* Analogous to sending `Authorization: Bearer <token>` (RFC 6750 section 2.1).
*/
export interface IAuthenticateParams {
/**
* The `resource` identifier from the server's
* {@link IAuthorizationProtectedResourceMetadata} that this token targets.
*/
readonly resource: string;

/** The bearer token value (RFC 6750). */
readonly token: string;
}

/**
* Result of the `authenticate` command.
*/
export interface IAuthenticateResult {
/** Whether the token was accepted. */
readonly authenticated: boolean;
}

export interface IAgentCreateSessionConfig {
readonly provider?: AgentProvider;
readonly model?: string;
Expand Down Expand Up @@ -301,8 +347,14 @@ export interface IAgent {
/** List persisted sessions from this provider. */
listSessions(): Promise<IAgentSessionMetadata[]>;

/** Set the authentication token for this provider. */
setAuthToken(token: string): Promise<void>;
/** Declare protected resources this agent requires auth for (RFC 9728). */
getProtectedResources(): IAuthorizationProtectedResourceMetadata[];

/**
* Authenticate for a specific resource. Returns true if accepted.
* The `resource` matches {@link IAuthorizationProtectedResourceMetadata.resource}.
*/
authenticate(resource: string, token: string): Promise<boolean>;

/** Gracefully shut down all sessions. */
shutdown(): Promise<void>;
Expand All @@ -329,8 +381,18 @@ export interface IAgentService {
/** Discover available agent backends from the agent host. */
listAgents(): Promise<IAgentDescriptor[]>;

/** Set the GitHub auth token used by the Copilot SDK. */
setAuthToken(token: string): Promise<void>;
/**
* Retrieve the resource metadata describing auth requirements.
* Modeled on RFC 9728 (OAuth 2.0 Protected Resource Metadata).
*/
getResourceMetadata(): Promise<IResourceMetadata>;

/**
* Authenticate for a protected resource on the server.
* The {@link IAuthenticateParams.resource} must match a resource from
* {@link getResourceMetadata}. Analogous to RFC 6750 bearer token delivery.
*/
authenticate(params: IAuthenticateParams): Promise<IAuthenticateResult>;

/**
* Refresh the model list from all providers, publishing updated
Expand Down
4 changes: 3 additions & 1 deletion src/vs/platform/agentHost/common/state/sessionProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export const AHP_SESSION_ALREADY_EXISTS = -32003 as const;
export const AHP_TURN_IN_PROGRESS = -32004 as const;
export const AHP_UNSUPPORTED_PROTOCOL_VERSION = -32005 as const;
export const AHP_CONTENT_NOT_FOUND = -32006 as const;
export const AHP_AUTH_REQUIRED = -32007 as const;

// ---- Type guards -----------------------------------------------------------

Expand All @@ -101,9 +102,10 @@ export function isJsonRpcResponse(msg: IProtocolMessage): msg is IAhpSuccessResp

/**
* Error with a JSON-RPC error code for protocol-level failures.
* Optionally carries a `data` payload for structured error details.
*/
export class ProtocolError extends Error {
constructor(readonly code: number, message: string) {
constructor(readonly code: number, message: string, readonly data?: unknown) {
super(message);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { acquirePort } from '../../../base/parts/ipc/electron-browser/ipc.mp.js'
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { ILogService } from '../../log/common/log.js';
import { AgentHostEnabledSettingId, AgentHostIpcChannels, IAgentCreateSessionConfig, IAgentDescriptor, IAgentHostService, IAgentService, IAgentSessionMetadata } from '../common/agentService.js';
import { AgentHostEnabledSettingId, AgentHostIpcChannels, IAgentCreateSessionConfig, IAgentDescriptor, IAgentHostService, IAgentService, IAgentSessionMetadata, IAuthenticateParams, IAuthenticateResult, IResourceMetadata } from '../common/agentService.js';
import type { IActionEnvelope, INotification, ISessionAction } from '../common/state/sessionActions.js';
import type { IBrowseDirectoryResult, IStateSnapshot } from '../common/state/sessionProtocol.js';
import { revive } from '../../../base/common/marshalling.js';
Expand Down Expand Up @@ -83,8 +83,11 @@ class AgentHostServiceClient extends Disposable implements IAgentHostService {

// ---- IAgentService forwarding (no await needed, delayed channel handles queuing) ----

setAuthToken(token: string): Promise<void> {
return this._proxy.setAuthToken(token);
getResourceMetadata(): Promise<IResourceMetadata> {
return this._proxy.getResourceMetadata();
}
authenticate(params: IAuthenticateParams): Promise<IAuthenticateResult> {
return this._proxy.authenticate(params);
}
listAgents(): Promise<IAgentDescriptor[]> {
return this._proxy.listAgents();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { hasKey } from '../../../base/common/types.js';
import { URI } from '../../../base/common/uri.js';
import { generateUuid } from '../../../base/common/uuid.js';
import { ILogService } from '../../log/common/log.js';
import { AgentSession, IAgentConnection, IAgentCreateSessionConfig, IAgentDescriptor, IAgentSessionMetadata } from '../common/agentService.js';
import { AgentSession, IAgentConnection, IAgentCreateSessionConfig, IAgentDescriptor, IAgentSessionMetadata, IAuthenticateParams, IAuthenticateResult, IResourceMetadata } from '../common/agentService.js';
import type { IClientNotificationMap, ICommandMap } from '../common/state/protocol/messages.js';
import type { IActionEnvelope, INotification, ISessionAction } from '../common/state/sessionActions.js';
import { PROTOCOL_VERSION } from '../common/state/sessionCapabilities.js';
Expand Down Expand Up @@ -128,10 +128,17 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
}

/**
* Push a GitHub auth token to the remote agent host.
* Retrieve the server's resource metadata describing auth requirements.
*/
async setAuthToken(token: string): Promise<void> {
this._sendExtensionNotification('setAuthToken', { token });
async getResourceMetadata(): Promise<IResourceMetadata> {
return await this._sendExtensionRequest('getResourceMetadata') as IResourceMetadata;
}

/**
* Authenticate with the remote agent host using a specific scheme.
*/
async authenticate(params: IAuthenticateParams): Promise<IAuthenticateResult> {
return await this._sendExtensionRequest('authenticate', params) as IAuthenticateResult;
}

/**
Expand Down Expand Up @@ -227,13 +234,6 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
this._transport.send({ jsonrpc: '2.0' as const, method, params } as IProtocolMessage);
}

/** Send a JSON-RPC notification for a VS Code extension method (not in the protocol spec). */
private _sendExtensionNotification(method: string, params?: unknown): void {
// Cast: extension methods aren't in the typed protocol maps yet
// eslint-disable-next-line local/code-no-dangerous-type-assertions
this._transport.send({ jsonrpc: '2.0', method, params } as unknown as IJsonRpcResponse);
}

/** Send a typed JSON-RPC request for a protocol-defined method. */
private _sendRequest<M extends keyof ICommandMap>(method: M, params: ICommandMap[M]['params']): Promise<ICommandMap[M]['result']> {
const id = this._nextRequestId++;
Expand Down
8 changes: 6 additions & 2 deletions src/vs/platform/agentHost/node/agentHostMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,12 @@ async function startWebSocketServer(agentService: AgentService, logService: ILog
modifiedAt: s.modifiedTime,
}));
},
handleSetAuthToken(token) {
agentService.setAuthToken(token);

handleGetResourceMetadata() {
return agentService.getResourceMetadataSync();
},
async handleAuthenticate(params) {
return agentService.authenticate(params);
},
handleBrowseDirectory(uri) {
return agentService.browseDirectory(URI.parse(uri));
Expand Down
Loading
Loading