Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ce19b8e
Refactor theme and layout for sidebar, panel, and auxiliary component…
mrleemurray Mar 24, 2026
c0a0761
Fix `get_terminal_output` Invalid ID Handling and Clarify Background …
meganrogge Mar 24, 2026
6973c35
Sessions - expose session resource to actions (#304497)
lszomoru Mar 24, 2026
0f2816f
Update distro commit (main) (#304506)
vs-code-engineering[bot] Mar 24, 2026
81d9831
browser: prevent new tab from flashing in quick pick (#304297)
jonathanrao99 Mar 24, 2026
0848b91
prompts: fix agent plugin hooks being picked up multiple times (#304523)
connor4312 Mar 24, 2026
4feb37f
settings: use local StopWatch to avoid timing corruption between conc…
xingsy97 Mar 24, 2026
020636f
Fix remote agent host file picker for new URI scheme (#304541)
roblourens Mar 24, 2026
16b26aa
Add "remove" button to remote picker (#304535)
roblourens Mar 24, 2026
5bbfc4b
Fix remote AH enablement setting name (#304547)
roblourens Mar 24, 2026
f42f9b1
Make "attach" only a QuickAccess thing, not a QuickPick thing (#304521)
TylerLeonhardt Mar 24, 2026
be51e1f
Fix subagent hook (#304543)
pwang347 Mar 24, 2026
fad70f3
set chat tips to true, always elevate AI profiles (#304553)
meganrogge Mar 24, 2026
2572ad4
agentPlugins: allow targeting a specific plugin install in deep link …
connor4312 Mar 24, 2026
4f9068c
Use skill folder as primary identifier (#304316)
pwang347 Mar 24, 2026
d55d222
Fix untitled session in sessions lists
osortega Mar 24, 2026
ad32a00
Merge pull request #304572 from microsoft/osortega/general-hare
osortega Mar 24, 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
13 changes: 12 additions & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,17 @@
"runOptions": {
"runOn": "worktreeCreated"
}
},
{
"label": "Echo E2E Status",
"type": "shell",
"command": "pwsh",
"args": [
"-NoProfile",
"-Command",
"Write-Output \"134 passed, 0 failed, 1 skipped, 135 total\"; Start-Sleep -Seconds 2; Write-Output \"[PASS] E2E Tests\"; Write-Output \"Watching for changes...\""
],
"isBackground": false
}
]
}
}
6 changes: 6 additions & 0 deletions build/lib/stylelint/vscode-known-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,12 @@
"--vscode-searchEditor-findMatchBorder",
"--vscode-searchEditor-textInputBorder",
"--vscode-selection-background",
"--vscode-sessionsAuxiliaryBar-background",
"--vscode-sessionsChatBar-background",
"--vscode-sessionsPanel-background",
"--vscode-sessionsSidebar-background",
"--vscode-sessionsSidebarHeader-background",
"--vscode-sessionsSidebarHeader-foreground",
"--vscode-sessionsUpdateButton-downloadedBackground",
"--vscode-sessionsUpdateButton-downloadingBackground",
"--vscode-settings-checkboxBackground",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.114.0",
"distro": "6c98cfe8dd3b4c159d8c9c331006a2d7c41872f0",
"distro": "d48ce3e61fa56a702d13bb450ceb3532620dbd46",
"author": {
"name": "Microsoft Corporation"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,6 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
}
}));

// Attach the active symbol as context
disposables.add(picker.onDidAttach(() => {
const [item] = picker.activeItems;
if (typeof item?.attach === 'function') {
item.attach();
}
}));

// Resolve symbols from document once and reuse this
// request for all filtering and typing then on
const symbolsPromise = this.getDocumentSymbols(model, token);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,15 @@ export class AgentHostFileSystemProvider extends Disposable implements IFileSyst
async stat(resource: URI): Promise<IStat> {
const path = resource.path;

// Root directory
// Root directory - either the bare scheme root or the root of the
// decoded remote filesystem (e.g. `/file/-/` decodes to `file:///`).
if (path === '/' || path === '') {
return { type: FileType.Directory, mtime: 0, ctime: 0, size: 0, permissions: FilePermission.Readonly };
}
const decodedPath = fromAgentHostUri(resource).path;
if (decodedPath === '/' || decodedPath === '') {
return { type: FileType.Directory, mtime: 0, ctime: 0, size: 0, permissions: FilePermission.Readonly };
}

// Use URI dirname/basename to find the parent and entry name
const parentUri = dirname(resource);
Expand Down
9 changes: 8 additions & 1 deletion src/vs/platform/agentHost/common/remoteAgentHostService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { IAgentConnection } from './agentService.js';
export const RemoteAgentHostsSettingId = 'chat.remoteAgentHosts';

/** Configuration key to enable remote agent host connections. */
export const RemoteAgentHostsEnabledSettingId = 'chat.remoteAgentHosts.enabled';
export const RemoteAgentHostsEnabledSettingId = 'chat.remoteAgentHostsEnabled';

/** An entry in the {@link RemoteAgentHostsSettingId} setting. */
export interface IRemoteAgentHostEntry {
Expand Down Expand Up @@ -69,6 +69,12 @@ export interface IRemoteAgentHostService {
* to that host is available.
*/
addRemoteAgentHost(entry: IRemoteAgentHostEntry): Promise<IRemoteAgentHostConnectionInfo>;

/**
* Removes a configured remote host entry by address.
* Disconnects any active connection and removes the entry from settings.
*/
removeRemoteAgentHost(address: string): Promise<void>;
}

/** Metadata about a single remote connection. */
Expand All @@ -88,6 +94,7 @@ export class NullRemoteAgentHostService implements IRemoteAgentHostService {
async addRemoteAgentHost(): Promise<IRemoteAgentHostConnectionInfo> {
throw new Error('Remote agent host connections are not supported in this environment.');
}
async removeRemoteAgentHost(_address: string): Promise<void> { }
}

export function parseRemoteAgentHostInput(input: string): RemoteAgentHostInputParseResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,21 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo
return connection;
}

async removeRemoteAgentHost(address: string): Promise<void> {
const normalized = normalizeRemoteAgentHostAddress(address);
// This setting is only used in the sessions app (user scope), so we
// don't need to inspect per-scope values like _upsertConfiguredEntry does.
const entries = this._getConfiguredEntries().filter(
e => normalizeRemoteAgentHostAddress(e.address) !== normalized
);
await this._storeConfiguredEntries(entries);

// Eagerly clear in-memory state so the UI updates immediately
// (the config change listener will reconcile, but this is instant).
this._names.delete(normalized);
this._removeConnection(normalized);
}

private _removeConnection(address: string): void {
const entry = this._entries.get(address);
if (entry) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import assert from 'assert';
import { URI } from '../../../../base/common/uri.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
import { agentHostRemotePath, agentHostUri } from '../../common/agentHostFileSystemProvider.js';
import { AGENT_HOST_SCHEME, agentHostAuthority, fromAgentHostUri, toAgentHostUri } from '../../common/agentHostUri.js';
import { AGENT_HOST_LABEL_FORMATTER, AGENT_HOST_SCHEME, agentHostAuthority, fromAgentHostUri, toAgentHostUri } from '../../common/agentHostUri.js';

suite('AgentHostFileSystemProvider - URI helpers', () => {

Expand Down Expand Up @@ -121,10 +121,58 @@ suite('toAgentHostUri / fromAgentHostUri', () => {
assert.strictEqual(result.toString(), original.toString());
});

test('agentHostUri for root path produces valid encoded URI', () => {
const authority = agentHostAuthority('localhost:8089');
const uri = agentHostUri(authority, '/');
assert.strictEqual(uri.scheme, AGENT_HOST_SCHEME);
assert.strictEqual(uri.authority, authority);
// The decoded path should be root
assert.strictEqual(fromAgentHostUri(uri).path, '/');
});

test('fromAgentHostUri handles malformed path gracefully', () => {
const uri = URI.from({ scheme: AGENT_HOST_SCHEME, authority: 'host', path: '/file' });
const result = fromAgentHostUri(uri);
// Should not throw falls back to extracting scheme only
// Should not throw - falls back to extracting scheme only
assert.strictEqual(result.scheme, 'file');
});
});

suite('AGENT_HOST_LABEL_FORMATTER', () => {

ensureNoDisposablesAreLeakedInTestSuite();

/**
* Replicates the stripPathSegments logic from the label service to
* verify that the formatter's configuration is consistent with the
* URI encoding.
*/
function stripPath(path: string, segments: number): string {
let pos = 0;
for (let i = 0; i < segments; i++) {
const next = path.indexOf('/', pos + 1);
if (next === -1) {
break;
}
pos = next;
}
return path.substring(pos);
}

test('stripPathSegments matches URI encoding for file URIs', () => {
const authority = agentHostAuthority('localhost:8089');
const originalPath = '/Users/roblou/code/vscode';
const encodedUri = agentHostUri(authority, originalPath);

const stripped = stripPath(encodedUri.path, AGENT_HOST_LABEL_FORMATTER.formatting.stripPathSegments!);
assert.strictEqual(stripped, originalPath);
});

test('stripPathSegments matches URI encoding with authority', () => {
const originalUri = URI.from({ scheme: 'agenthost-content', authority: 'myhost', path: '/snap/before' });
const encodedUri = toAgentHostUri(originalUri, 'remote-host');

const stripped = stripPath(encodedUri.path, AGENT_HOST_LABEL_FORMATTER.formatting.stripPathSegments!);
assert.strictEqual(stripped, '/snap/before');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -393,4 +393,36 @@ suite('RemoteAgentHostService', () => {
await Event.toPromise(service.onDidChangeConnections);
assert.strictEqual(service.connections.length, 1);
});

test('removeRemoteAgentHost removes entry and disconnects', async () => {
configService.setEntries([
{ address: 'ws://host1:8080', name: 'Host 1' },
{ address: 'ws://host2:9090', name: 'Host 2' },
]);
createdClients[0].connectDeferred.complete();
await Event.toPromise(service.onDidChangeConnections);
createdClients[1].connectDeferred.complete();
await Event.toPromise(service.onDidChangeConnections);
assert.strictEqual(service.connections.length, 2);

await service.removeRemoteAgentHost('ws://host1:8080');

assert.deepStrictEqual(configService.entries, [
{ address: 'ws://host2:9090', name: 'Host 2' },
]);
assert.strictEqual(service.connections.length, 1);
assert.strictEqual(service.getConnection('ws://host1:8080'), undefined);
assert.ok(service.getConnection('ws://host2:9090'));
});

test('removeRemoteAgentHost normalizes address before removing', async () => {
configService.setEntries([{ address: 'host1:8080', name: 'Host 1' }]);
createdClients[0].connectDeferred.complete();
await Event.toPromise(service.onDidChangeConnections);

await service.removeRemoteAgentHost('ws://host1:8080');

assert.deepStrictEqual(configService.entries, []);
assert.strictEqual(service.connections.length, 0);
});
});
29 changes: 15 additions & 14 deletions src/vs/platform/quickinput/browser/pickerQuickAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { timeout } from '../../../base/common/async.js';
import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js';
import { IKeyMods, IQuickPickDidAcceptEvent, IQuickPickSeparator, IQuickPick, IQuickPickItem, IQuickInputButton } from '../common/quickInput.js';
import { IKeyMods, IQuickPickDidAcceptEvent, IQuickPickSeparator, IQuickPick, IQuickPickItem, IQuickInputButton, isKeyModified } from '../common/quickInput.js';
import { IQuickAccessProvider, IQuickAccessProviderRunOptions } from '../common/quickAccess.js';
import { isFunction } from '../../../base/common/types.js';

Expand Down Expand Up @@ -51,18 +51,22 @@ export interface IPickerQuickAccessItem extends IQuickPickItem {
* @param buttonIndex index of the button of the item that
* was clicked.
*
* @param the state of modifier keys when the button was triggered.
* @param keyMods the state of modifier keys when the button was triggered.
*
* @returns a value that indicates what should happen after the trigger
* which can be a `Promise` for long running operations.
*/
trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise<TriggerAction>;

/**
* A method that will be executed when the pick item should be attached
* as context, e.g. to a chat conversation.
* When set, this will be invoked instead of `accept` if modifier keys are held down.
* This is useful for actions like "attach to context" where you want to keep the picker
* open and allow multiple picks.
*
* @param keyMods the state of modifier keys when the item was accepted.
* @param event the underlying event that caused this to trigger.
*/
attach?(): void;
attach?(keyMods: IKeyMods, event: IQuickPickDidAcceptEvent): void;
}

export interface IPickerQuickAccessSeparator extends IQuickPickSeparator {
Expand All @@ -73,7 +77,7 @@ export interface IPickerQuickAccessSeparator extends IQuickPickSeparator {
* @param buttonIndex index of the button of the item that
* was clicked.
*
* @param the state of modifier keys when the button was triggered.
* @param keyMods the state of modifier keys when the button was triggered.
*
* @returns a value that indicates what should happen after the trigger
* which can be a `Promise` for long running operations.
Expand Down Expand Up @@ -343,6 +347,11 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem

const [item] = picker.selectedItems;
if (typeof item?.accept === 'function') {
const isAttachAction = isKeyModified(picker.keyMods) && !!item.attach;
if (isAttachAction) {
item.attach!(picker.keyMods, event);
return;
}
if (!event.inBackground) {
picker.hide(); // hide picker unless we accept in background
}
Expand All @@ -351,14 +360,6 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
}
}));

// Attach the active pick item as context
disposables.add(picker.onDidAttach(() => {
const [item] = picker.activeItems;
if (typeof item?.attach === 'function') {
item.attach();
}
}));

const buttonTrigger = async (button: IQuickInputButton, item: T | IPickerQuickAccessSeparator) => {
if (typeof item.trigger !== 'function') {
return;
Expand Down
7 changes: 0 additions & 7 deletions src/vs/platform/quickinput/browser/quickInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,6 @@ export class QuickPick<T extends IQuickPickItem, O extends { useSeparators: bool
private readonly onWillAcceptEmitter = this._register(new Emitter<IQuickPickWillAcceptEvent>());
private readonly onDidAcceptEmitter = this._register(new Emitter<IQuickPickDidAcceptEvent>());
private readonly onDidCustomEmitter = this._register(new Emitter<void>());
private readonly onDidAttachEmitter = this._register(new Emitter<void>());
private _items: O extends { useSeparators: true } ? Array<T | IQuickPickSeparator> : Array<T> = [];
private itemsUpdated = false;
private _canSelectMany = false;
Expand Down Expand Up @@ -644,8 +643,6 @@ export class QuickPick<T extends IQuickPickItem, O extends { useSeparators: bool

onDidCustom = this.onDidCustomEmitter.event;

onDidAttach = this.onDidAttachEmitter.event;

get items() {
return this._items;
}
Expand Down Expand Up @@ -1180,10 +1177,6 @@ export class QuickPick<T extends IQuickPickItem, O extends { useSeparators: bool
}
this.handleAccept(inBackground ?? false);
}

attach(): void {
this.onDidAttachEmitter.fire();
}
}

export class InputBox extends QuickInput implements IInputBox {
Expand Down
36 changes: 14 additions & 22 deletions src/vs/platform/quickinput/browser/quickInputActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function registerQuickPickCommandAndKeybindingRule(rule: PartialExcept<ICommandA
const ctrlKeyMod = isMacintosh ? KeyMod.WinCtrl : KeyMod.CtrlCmd;

// This function will generate all the combinations of keybindings for the given primary keybinding
function getSecondary(primary: number, secondary: number[], options: { withAltMod?: boolean; withCtrlMod?: boolean; withCmdMod?: boolean } = {}): number[] {
function getSecondary(primary: number, secondary: number[], options: { withAltMod?: boolean; withCtrlMod?: boolean; withCmdMod?: boolean; withShiftMod?: boolean } = {}): number[] {
if (options.withAltMod) {
secondary.push(KeyMod.Alt + primary);
}
Expand All @@ -54,6 +54,18 @@ function getSecondary(primary: number, secondary: number[], options: { withAltMo
secondary.push(KeyMod.Alt + ctrlKeyMod + primary);
}
}
if (options.withShiftMod) {
secondary.push(KeyMod.Shift + primary);
if (options.withAltMod) {
secondary.push(KeyMod.Alt + KeyMod.Shift + primary);
}
if (options.withCtrlMod) {
secondary.push(ctrlKeyMod + KeyMod.Shift + primary);
if (options.withAltMod) {
secondary.push(KeyMod.Alt + ctrlKeyMod + KeyMod.Shift + primary);
}
}
}

if (options.withCmdMod && isMacintosh) {
secondary.push(KeyMod.CtrlCmd + primary);
Expand Down Expand Up @@ -213,7 +225,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
const currentQuickPick = accessor.get(IQuickInputService).currentQuickInput as IQuickPick<any> | IQuickTree<any> | IInputBox;
currentQuickPick?.accept();
},
secondary: getSecondary(KeyCode.Enter, [], { withAltMod: true, withCtrlMod: true, withCmdMod: true })
secondary: getSecondary(KeyCode.Enter, [], { withAltMod: true, withCtrlMod: true, withCmdMod: true, withShiftMod: true })
});

registerQuickPickCommandAndKeybindingRule(
Expand All @@ -239,26 +251,6 @@ registerQuickPickCommandAndKeybindingRule(

//#endregion

//#region Attach

registerQuickPickCommandAndKeybindingRule(
{
id: 'quickInput.attach',
when: ContextKeyExpr.and(
inQuickInputContext,
ContextKeyExpr.equals(quickInputTypeContextKeyValue, QuickInputType.QuickPick),
),
primary: KeyMod.Shift | KeyCode.Enter,
weight: KeybindingWeight.WorkbenchContrib + 100,
handler: (accessor) => {
const currentQuickPick = accessor.get(IQuickInputService).currentQuickInput as IQuickPick<any>;
currentQuickPick?.attach();
},
},
);

//#endregion

//#region Hide

registerQuickInputCommandAndKeybindingRule(
Expand Down
Loading
Loading