Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
22ca50c
Initial plan
Copilot Mar 11, 2026
9c2b703
Fix contextview viewport calculation to use container's window instea…
Copilot Mar 11, 2026
5367daf
feat(telemetry): log terminal sandbox setting changes
isidorn Mar 26, 2026
cda51ae
Merge branch 'main' into isidorn/private-pig
rzhao271 Mar 26, 2026
404856f
Merge branch 'main' into isidorn/private-pig
rzhao271 Mar 26, 2026
c1a61a4
Merge branch 'main' into copilot/fix-floating-window-focus-issue
mjbvz Mar 27, 2026
228f1b6
Merge pull request #304959 from yogeshwaran-c/fix/testing-icon-color-…
yogeshwaran-c Mar 27, 2026
1d62cc6
agentPlugins: normalize to user data dir storage (#304977)
connor4312 Mar 27, 2026
6afe980
Preserve $TMPDIR when retrying terminal commands outside the sandbox …
dileepyavan Mar 27, 2026
3bafa7d
fix for chat tips (#304899)
meganrogge Mar 27, 2026
befae3e
timeline: fix memory leak when toggling pane visibility (#304668)
xingsy97 Mar 27, 2026
d1058a0
debt - clean up some todos (#305530)
bpasero Mar 27, 2026
ddf0a3f
Merge pull request #300691 from microsoft/copilot/fix-floating-window…
mjbvz Mar 27, 2026
d6638bd
fix some thinking content rendering for edits + lazy markdown not ren…
justschen Mar 27, 2026
e78b8eb
Fix inconsistent capitalization in permissions learn-more string (#30…
Copilot Mar 27, 2026
5f966ed
Also default to treating macos as case insensitive for md file checks
mjbvz Mar 27, 2026
3b3b067
make tool call confirmation content LARGER (#305538)
justschen Mar 27, 2026
bd5b480
Use `areUrisEqual` helper for better uri checks
mjbvz Mar 27, 2026
e54bc08
Merge pull request #305548 from mjbvz/dev/mjbvz/related-rat
mjbvz Mar 27, 2026
87d56f7
Merge pull request #305550 from mjbvz/dev/mjbvz/tense-dragon
mjbvz Mar 27, 2026
1049842
use new session icon instead of plus for add chat action
benibenj Mar 27, 2026
9c09f68
Merge pull request #305156 from microsoft/isidorn/private-pig
isidorn Mar 27, 2026
9986a43
Merge pull request #305569 from microsoft/benibenj/unusual-iguana
benibenj Mar 27, 2026
7c89420
Remove ChatAgentVoteDownReason and voteDownReason (#304878)
isidorn Mar 27, 2026
78a3908
Skills usage telemetry (#303110)
AbhitejJohn Mar 27, 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
5 changes: 3 additions & 2 deletions extensions/markdown-language-features/src/preview/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,9 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
const watcher = this._register(vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(resource, '*')));
this._register(watcher.onDidChange(uri => {
if (this.isPreviewOf(uri)) {
// Only use the file system event when VS Code does not already know about the file
if (!vscode.workspace.textDocuments.some(doc => doc.uri.toString() === uri.toString())) {
// Only use the file system event when VS Code does not already know about the file.
// This is needed to avoid duplicate refreshes
if (!vscode.workspace.textDocuments.some(doc => areUrisEqual(doc.uri, uri))) {
this.refresh();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function areUrisEqual(uri1: vscode.Uri, uri2: vscode.Uri): boolean {
}

if (uri1.scheme === 'file') {
if (process.platform === 'win32') {
if (process.platform === 'win32' || process.platform === 'darwin') {
return uri1.fsPath.toLowerCase() === uri2.fsPath.toLowerCase();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function extractTextContent(result: vscode.LanguageModelToolResult): string {
participantRegistered = false;
pendingResult = undefined;
pendingCommand = undefined;
pendingTimeout = undefined;
pendingOptions = undefined;

const chatToolsConfig = vscode.workspace.getConfiguration('chat.tools.global');
await chatToolsConfig.update('autoApprove', undefined, vscode.ConfigurationTarget.Global);
Expand All @@ -82,10 +82,16 @@ function extractTextContent(result: vscode.LanguageModelToolResult): string {
* Helper: invokes run_in_terminal via a chat participant and returns the tool result text.
* Each call creates a new chat session to avoid participant re-registration issues.
*/
interface RunInTerminalOptions {
timeout?: number;
requestUnsandboxedExecution?: boolean;
requestUnsandboxedExecutionReason?: string;
}

let participantRegistered = false;
let pendingResult: DeferredPromise<vscode.LanguageModelToolResult> | undefined;
let pendingCommand: string | undefined;
let pendingTimeout: number | undefined;
let pendingOptions: RunInTerminalOptions | undefined;

function setupParticipant() {
if (participantRegistered) {
Expand All @@ -98,18 +104,22 @@ function extractTextContent(result: vscode.LanguageModelToolResult): string {
}
const currentResult = pendingResult;
const currentCommand = pendingCommand;
const currentTimeout = pendingTimeout ?? 15000;
const currentOptions = pendingOptions ?? {};
pendingResult = undefined;
pendingCommand = undefined;
pendingTimeout = undefined;
pendingOptions = undefined;
try {
const result = await vscode.lm.invokeTool('run_in_terminal', {
input: {
command: currentCommand,
explanation: 'Integration test command',
goal: 'Test run_in_terminal output',
isBackground: false,
timeout: currentTimeout
timeout: currentOptions.timeout ?? 15000,
...currentOptions.requestUnsandboxedExecution ? {
requestUnsandboxedExecution: true,
requestUnsandboxedExecutionReason: currentOptions.requestUnsandboxedExecutionReason,
} : {},
},
toolInvocationToken: request.toolInvocationToken,
});
Expand All @@ -122,13 +132,18 @@ function extractTextContent(result: vscode.LanguageModelToolResult): string {
disposables.push(participant);
}

async function invokeRunInTerminal(command: string, timeout = 15000): Promise<string> {
async function invokeRunInTerminal(command: string, options?: RunInTerminalOptions): Promise<string>;
async function invokeRunInTerminal(command: string, timeout?: number): Promise<string>;
async function invokeRunInTerminal(command: string, optionsOrTimeout?: RunInTerminalOptions | number): Promise<string> {
setupParticipant();

const opts: RunInTerminalOptions = typeof optionsOrTimeout === 'number'
? { timeout: optionsOrTimeout }
: optionsOrTimeout ?? {};
const resultPromise = new DeferredPromise<vscode.LanguageModelToolResult>();
pendingResult = resultPromise;
pendingCommand = command;
pendingTimeout = timeout;
pendingOptions = opts;

await vscode.commands.executeCommand('workbench.action.chat.newChat');
vscode.commands.executeCommand('workbench.action.chat.open', { query: '@participant test' });
Expand Down Expand Up @@ -326,6 +341,27 @@ function extractTextContent(result: vscode.LanguageModelToolResult): string {
assert.ok(acceptable.includes(output.trim()), `Unexpected output: ${JSON.stringify(output.trim())}`);
});

test('requestUnsandboxedExecution preserves sandbox $TMPDIR', async function () {
this.timeout(60000);

const marker = `SANDBOX_UNSANDBOX_${Date.now()}`;
const sentinelName = `sentinel-${marker}.txt`;

// Step 1: Write a sentinel file into the sandbox-provided $TMPDIR.
const writeOutput = await invokeRunInTerminal(`echo ${marker} > "$TMPDIR/${sentinelName}" && echo ${marker}`);
assert.strictEqual(writeOutput.trim(), marker);

// Step 2: Retry with requestUnsandboxedExecution=true while sandbox
// stays enabled. The tool should preserve $TMPDIR from the sandbox so
// the sentinel file created in step 1 is still accessible.
const retryOutput = await invokeRunInTerminal(`cat "$TMPDIR/${sentinelName}"`, {
timeout: 30000,
requestUnsandboxedExecution: true,
requestUnsandboxedExecutionReason: 'Need to verify $TMPDIR persists on unsandboxed retry',
});
assert.strictEqual(retryOutput.trim(), marker);
});

test('cannot write to /tmp', async function () {
this.timeout(60000);

Expand Down
4 changes: 2 additions & 2 deletions src/vs/base/browser/ui/contextview/contextview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,8 @@ export class ContextView extends Disposable {

// Get anchor
const anchor = getAnchorRect(this.delegate!.getAnchor());
const activeWindow = DOM.getActiveWindow();
const viewport = { top: activeWindow.pageYOffset, left: activeWindow.pageXOffset, width: activeWindow.innerWidth, height: activeWindow.innerHeight };
const containerWindow = this.container ? DOM.getWindow(this.container) : DOM.getActiveWindow();
const viewport = { top: containerWindow.pageYOffset, left: containerWindow.pageXOffset, width: containerWindow.innerWidth, height: containerWindow.innerHeight };
const view = { width: DOM.getTotalWidth(this.view), height: DOM.getTotalHeight(this.view) };
const anchorPosition = this.delegate!.anchorPosition;
const anchorAlignment = this.delegate!.anchorAlignment;
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/environment/common/argv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export interface NativeParsedArgs {
'extensions-dir'?: string;
'extensions-download-dir'?: string;
'builtin-extensions-dir'?: string;
'agent-plugins-dir'?: string;
extensionDevelopmentPath?: string[]; // undefined or array of 1 or more local paths or URIs
extensionTestsPath?: string; // either a local path or a URI
extensionDevelopmentKind?: string[];
Expand Down
20 changes: 20 additions & 0 deletions src/vs/platform/environment/common/environmentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,26 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron
return joinPath(this.userHome, this.productService.dataFolderName, 'extensions').fsPath;
}

@memoize
get agentPluginsPath(): string {
const cliAgentPluginsDir = this.args['agent-plugins-dir'];
if (cliAgentPluginsDir) {
return resolve(cliAgentPluginsDir);
}

const vscodeAgentPlugins = env['VSCODE_AGENT_PLUGINS'];
if (vscodeAgentPlugins) {
return vscodeAgentPlugins;
}

const vscodePortable = env['VSCODE_PORTABLE'];
if (vscodePortable) {
return join(vscodePortable, 'agent-plugins');
}

return joinPath(this.userHome, this.productService.dataFolderName, 'agent-plugins').fsPath;
}

@memoize
get extensionDevelopmentLocationURI(): URI[] | undefined {
const extensionDevelopmentPaths = this.args.extensionDevelopmentPath;
Expand Down
1 change: 1 addition & 0 deletions src/vs/platform/environment/node/argv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'extensions-download-dir': { type: 'string' },
'builtin-extensions-dir': { type: 'string' },
'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") },
'agent-plugins-dir': { type: 'string' },
'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extensions.") },
'category': { type: 'string', allowEmptyValue: true, cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions."), args: 'category' },
'install-extension': { type: 'string[]', cat: 'e', args: 'ext-id | path', description: localize('installExtension', "Installs or updates an extension. The argument is either an extension id or a path to a VSIX. The identifier of an extension is '${publisher}.${name}'. Use '--force' argument to update to latest version. To install a specific version provide '@${version}'. For example: 'vscode.csharp@1.2.3'.") },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,11 @@ export class NewChatPermissionPicker extends Disposable {
kind: ActionListItemKind.Action,
group: { kind: ActionListItemKind.Header, title: '', icon: Codicon.blank },
item: {
label: localize('permissions.learnMore', "Learn More about Permissions"),
label: localize('permissions.learnMore', "Learn more about permissions"),
icon: Codicon.blank,
checked: false,
},
label: localize('permissions.learnMore', "Learn More about Permissions"),
label: localize('permissions.learnMore', "Learn more about permissions"),
hideIcon: false,
disabled: false,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerDefaultCon
'files.watcherExclude': {
'**/.git/objects/**': true,
'**/.git/subtree-cache/**': true,
'**/node_modules/*/**': true /* TODO@bpasero see if this helps improve perf */,
'**/node_modules/*/**': true,
'**/.hg/store/**': true
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ registerAction2(class AddChatAction extends Action2 {
super({
id: 'agentSession.addChat',
title: localize2('addChat', "Add Chat"),
icon: Codicon.plus,
icon: Codicon.newSession,
menu: [{
id: Menus.CommandCenter,
order: 102,
Expand Down
1 change: 0 additions & 1 deletion src/vs/workbench/api/common/extHostChatAgents2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1038,7 +1038,6 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
const feedback: vscode.ChatResultFeedback = {
result: ehResult,
kind,
unhelpfulReason: isProposedApiEnabled(agent.extension, 'chatParticipantAdditions') ? voteAction.reason : undefined,
};
agent.acceptFeedback(Object.freeze(feedback));
}
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/browser/workbench.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
localize('useModal.all', "All editors open in a centered modal overlay."),
],
'description': localize('useModal', "Controls whether editors open in a modal overlay."),
'default': product.quality !== 'stable' ? 'some' : 'off', // TODO@bpasero figure out the default
'default': 'some',
tags: ['experimental'],
experiment: {
mode: 'auto'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { CellEditType, CellKind, NOTEBOOK_EDITOR_ID } from '../../../notebook/co
import { NOTEBOOK_IS_ACTIVE_EDITOR } from '../../../notebook/common/notebookContextKeys.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { applyingChatEditsFailedContextKey, isChatEditingActionContext } from '../../common/editing/chatEditingService.js';
import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatService } from '../../common/chatService/chatService.js';
import { ChatAgentVoteDirection, IChatService } from '../../common/chatService/chatService.js';
import { isResponseVM } from '../../common/model/chatViewModel.js';
import { ChatModeKind } from '../../common/constants.js';
import { IChatAccessibilityService, IChatWidgetService } from '../chat.js';
Expand Down Expand Up @@ -71,11 +71,9 @@ export function registerChatTitleActions() {
action: {
kind: 'vote',
direction: ChatAgentVoteDirection.Up,
reason: undefined
}
});
item.setVote(ChatAgentVoteDirection.Up);
item.setVoteDownReason(undefined);
}
});

Expand Down Expand Up @@ -108,13 +106,7 @@ export function registerChatTitleActions() {
return;
}

const reason = args[1];
if (typeof reason !== 'string') {
return;
}

item.setVote(ChatAgentVoteDirection.Down);
item.setVoteDownReason(reason as ChatAgentVoteDownReason);

const chatService = accessor.get(IChatService);
chatService.notifyUserAction({
Expand All @@ -126,7 +118,6 @@ export function registerChatTitleActions() {
action: {
kind: 'vote',
direction: ChatAgentVoteDirection.Down,
reason: item.voteDownReason
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { dirname, isEqual, isEqualOrParent, joinPath } from '../../../../base/co
import { URI } from '../../../../base/common/uri.js';
import { localize } from '../../../../nls.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { ILogService } from '../../../../platform/log/common/log.js';
Expand All @@ -36,22 +36,40 @@ type IStoredMarketplaceIndex = Dto<Record<string, IMarketplaceIndexEntry>>;
export class AgentPluginRepositoryService implements IAgentPluginRepositoryService {
declare readonly _serviceBrand: undefined;

readonly agentPluginsHome: URI;
private readonly _cacheRoot: URI;
private readonly _marketplaceIndex = new Lazy<Map<string, IMarketplaceIndexEntry>>(() => this._loadMarketplaceIndex());
private readonly _pluginSources: ReadonlyMap<PluginSourceKind, IPluginSource>;
private readonly _cloneSequencer = new SequencerByKey<string>();
private readonly _migrationDone: Promise<void>;

constructor(
@ICommandService private readonly _commandService: ICommandService,
@IEnvironmentService environmentService: IEnvironmentService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IFileService private readonly _fileService: IFileService,
@IInstantiationService instantiationService: IInstantiationService,
@ILogService private readonly _logService: ILogService,
@INotificationService private readonly _notificationService: INotificationService,
@IProgressService private readonly _progressService: IProgressService,
@IStorageService private readonly _storageService: IStorageService,
) {
this._cacheRoot = joinPath(environmentService.cacheHome, 'agentPlugins');
// On native, use the well-known ~/{dataFolderName}/agent-plugins/ path
// so that external tools can discover it. On web, fall back to the
// internal cache location.
this.agentPluginsHome = environmentService.agentPluginsHome;
const legacyCacheRoot = joinPath(environmentService.cacheHome, 'agentPlugins');
const oldCacheRoot = environmentService.cacheHome.scheme === 'file'
? legacyCacheRoot
: this.agentPluginsHome;
this._cacheRoot = this.agentPluginsHome;

// Migrate plugin files from the old internal cache directory to the
// new well-known location. This is a one-time operation.
if (!isEqual(oldCacheRoot, this.agentPluginsHome)) {
this._migrationDone = this._migrateDirectory(oldCacheRoot);
} else {
this._migrationDone = Promise.resolve();
}

// Build per-kind source repository map via instantiation service so
// each repository can inject its own dependencies.
Expand Down Expand Up @@ -91,6 +109,7 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi
}

async ensureRepository(marketplace: IMarketplaceReference, options?: IEnsureRepositoryOptions): Promise<URI> {
await this._migrationDone;
const repoDir = this.getRepositoryUri(marketplace, options?.marketplaceType);
return this._cloneSequencer.queue(repoDir.fsPath, async () => {
const repoExists = await this._fileService.exists(repoDir);
Expand Down Expand Up @@ -254,6 +273,7 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi
}

async ensurePluginSource(plugin: IMarketplacePlugin, options?: IEnsureRepositoryOptions): Promise<URI> {
await this._migrationDone;
const repo = this.getPluginSource(plugin.sourceDescriptor.kind);
if (plugin.sourceDescriptor.kind === PluginSourceKind.RelativePath) {
return this.ensureRepository(plugin.marketplaceReference, options);
Expand Down Expand Up @@ -351,4 +371,34 @@ export class AgentPluginRepositoryService implements IAgentPluginRepositoryServi
}
}

/**
* One-time migration of plugin files from the old internal cache
* directory (`{cacheHome}/agentPlugins/`) to the new well-known
* location (`~/{dataFolderName}/agent-plugins/`).
*/
private async _migrateDirectory(oldCacheRoot: URI): Promise<void> {
try {
const oldExists = await this._fileService.exists(oldCacheRoot);
if (!oldExists) {
return;
}

const newExists = await this._fileService.exists(this.agentPluginsHome);
if (newExists) {
this._logService.info('[AgentPluginRepositoryService] Both old and new agent-plugins directories exist; skipping directory migration');
return;
}

this._logService.info(`[AgentPluginRepositoryService] Migrating agent plugins from ${oldCacheRoot.toString()} to ${this.agentPluginsHome.toString()}`);
await this._fileService.move(oldCacheRoot, this.agentPluginsHome, false);

// Clear the marketplace index — it caches repository URIs that
// pointed to the old location and would cause path mismatches.
this._storageService.remove(MARKETPLACE_INDEX_STORAGE_KEY, StorageScope.APPLICATION);
this._marketplaceIndex.value.clear();
} catch (error) {
this._logService.error('[AgentPluginRepositoryService] Directory migration failed', error);
}
}

}
Loading
Loading