Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a013d5c
Sessions - increase polling interval for pull request data (#307608)
lszomoru Apr 3, 2026
63310be
Sessions - more perf improvements (#307627)
lszomoru Apr 3, 2026
32eeacc
Remove chat.statusWidget.anonymous
cwebster-99 Apr 3, 2026
38a5ee1
Update src/vs/workbench/contrib/chat/browser/widget/input/chatStatusW…
cwebster-99 Apr 3, 2026
cc5d7c8
testing: improve test coverage filter quickpick readability
yogeshwaran-c Apr 3, 2026
ffa49fc
testing: treat unrecognized @-prefixed text as regular filter in test…
yogeshwaran-c Apr 3, 2026
d0c21ae
feat: add ChatHookProvider to chatPromptFiles API (#307545)
joshspicer Apr 3, 2026
3d70aab
Sessions - improve merge skill + a minor fix (#307657)
lszomoru Apr 3, 2026
44b152c
feat: add plugins to chatPromptFiles API (#307669)
joshspicer Apr 3, 2026
46379c3
Fix error noise on browser tab close (#307674)
kycutler Apr 3, 2026
1394fbd
Fix reasoning effort menu being hidden behind browser (#307670)
kycutler Apr 3, 2026
75ef496
Default ignore skills and slash commands for other session types unle…
pwang347 Apr 3, 2026
20b2483
Remove debug event attachments (#307544)
pwang347 Apr 3, 2026
66639f8
chat: add startup telemetry for live chat model counts (#307678)
roblourens Apr 3, 2026
af760e0
Merge pull request #307643 from microsoft/arbitrary-pike
cwebster-99 Apr 3, 2026
0805547
agentHost: archive and store archive state for remote agent host sess…
roblourens Apr 3, 2026
89d0d13
agentPlugins: clone locally when in a remote (#303606)
connor4312 Apr 3, 2026
3c60b09
Sessions: preserve cancelled session content in list (#307684)
osortega Apr 3, 2026
e2694d7
Sessions - more cleanup around state management (#307694)
lszomoru Apr 3, 2026
b9d09e3
sessions: disable branch picker in folder mode (#307692)
hawkticehurst Apr 3, 2026
36824e3
sessions: show branch in active session title (#307711)
hawkticehurst Apr 3, 2026
6b7e153
sessions: reduce left sidebar minimum width (#307709)
hawkticehurst Apr 3, 2026
b3afb80
agentHost: fix editing (#307721)
connor4312 Apr 3, 2026
9b2810b
Let new browser tabs outlive their parents (#307726)
kycutler Apr 3, 2026
885fbb6
Fix: Clicking settings link in bool setting description incorrectly t…
Copilot Apr 3, 2026
51c3548
Agent Debug Panel: pagination, incremental filtering, and service opt…
vijayupadya Apr 3, 2026
de4547a
Browser: don't show a loading spinner for in-page navigation (#307728)
kycutler Apr 3, 2026
5fa9107
Snapshot browser state before disposal (#307734)
kycutler Apr 3, 2026
b564ded
agentHost: Validate strings in bash commands (#307699)
roblourens Apr 3, 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
10 changes: 10 additions & 0 deletions extensions/git/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,16 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi
this.logger.info(`[Model][openRepository] Opened repository (real path): ${repository.rootRealPath ?? repository.root}`);
this.logger.info(`[Model][openRepository] Opened repository (kind): ${gitRepository.kind}`);

// For repositories that are opened in the sessions app, we want to wait for
// the initial `git status` to complete before updating the repository cache
// and firing events.
if (workspace.isAgentSessionsWorkspace) {
await repository.status();
this._repositoryCache.update(repository.remotes, [], repository.root);

return;
}

// Do not await this, we want SCM
// to know about the repo asap
repository.status().then(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ import { McpGalleryManifestIPCService } from '../../../platform/mcp/common/mcpGa
import { IMeteredConnectionService } from '../../../platform/meteredConnection/common/meteredConnection.js';
import { MeteredConnectionChannelClient, METERED_CONNECTION_CHANNEL } from '../../../platform/meteredConnection/common/meteredConnectionIpc.js';
import { PlaywrightChannel } from '../../../platform/browserView/node/playwrightChannel.js';
import { ILocalGitService } from '../../../platform/git/common/localGitService.js';
import { LocalGitService } from '../../../platform/git/node/localGitService.js';

class SharedProcessMain extends Disposable implements IClientConnectionFilter {

Expand Down Expand Up @@ -404,6 +406,9 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
// Web Content Extractor
services.set(ISharedWebContentExtractorService, new SyncDescriptor(SharedWebContentExtractorService));

// Local Git
services.set(ILocalGitService, new SyncDescriptor(LocalGitService, undefined, false /* proxied to other processes */));

// SSH Remote Agent Host
services.set(ISSHRemoteAgentHostMainService, new SyncDescriptor(SSHRemoteAgentHostMainService, undefined, true));

Expand Down Expand Up @@ -478,6 +483,10 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
const playwrightChannel = this._register(new PlaywrightChannel(this.server, accessor.get(IMainProcessService), accessor.get(ILogService)));
this.server.registerChannel('playwright', playwrightChannel);

// Local Git
const localGitChannel = ProxyChannel.fromService(accessor.get(ILocalGitService), this._store);
this.server.registerChannel('localGit', localGitChannel);

// SSH Remote Agent Host
const sshRemoteAgentHostChannel = ProxyChannel.fromService(accessor.get(ISSHRemoteAgentHostMainService), this._store);
this.server.registerChannel(SSH_REMOTE_AGENT_HOST_CHANNEL, sshRemoteAgentHostChannel);
Expand Down
23 changes: 19 additions & 4 deletions src/vs/platform/agentHost/node/sshRemoteAgentHostService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,28 @@ interface SSHClient {

const LOG_PREFIX = '[SSHRemoteAgentHost]';

/**
* Validate that a quality string is safe for bare interpolation in shell commands.
* Quality comes from `productService.quality` (not user input) but we validate
* as defense-in-depth since these values end up in unquoted shell paths (the `~`
* prefix requires shell expansion, so we cannot single-quote the entire path).
*/
function validateShellToken(value: string, label: string): string {
if (!/^[a-zA-Z0-9._-]+$/.test(value)) {
throw new Error(`Unsafe ${label} value for shell interpolation: ${JSON.stringify(value)}`);
}
return value;
}

/** Install location for the VS Code CLI on the remote machine. */
function getRemoteCLIDir(quality: string): string {
return quality === 'stable' || !quality ? '~/.vscode-cli' : `~/.vscode-cli-${quality}`;
const q = validateShellToken(quality, 'quality');
return q === 'stable' ? '~/.vscode-cli' : `~/.vscode-cli-${q}`;
}
function getRemoteCLIBin(quality: string): string {
const binaryName = quality === 'stable' ? 'code' : 'code-insiders';
return `${getRemoteCLIDir(quality)}/${binaryName}`;
const q = validateShellToken(quality, 'quality');
const binaryName = q === 'stable' ? 'code' : 'code-insiders';
return `${getRemoteCLIDir(q)}/${binaryName}`;
}

/** Escape a string for use as a single shell argument (single-quote wrapping). */
Expand Down Expand Up @@ -653,7 +668,7 @@ export class SSHRemoteAgentHostMainService extends Disposable implements ISSHRem

const installCmd = [
`mkdir -p ${cliDir}`,
`curl -fsSL '${url}' | tar xz -C ${cliDir}`,
`curl -fsSL ${shellEscape(url)} | tar xz -C ${cliDir}`,
`chmod +x ${cliBin}`,
].join(' && ');

Expand Down
11 changes: 9 additions & 2 deletions src/vs/platform/browserView/electron-main/browserView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,10 @@ export class BrowserView extends Disposable implements ICDPTarget {

// Return the webContents so Electron can complete the window.open() call
return childView.webContents;
}
},

// We want the standard browser behavior as opposed to Electron's default of closing the new window when the parent is closed
outlivesOpener: true
};
});

Expand Down Expand Up @@ -236,7 +239,11 @@ export class BrowserView extends Disposable implements ICDPTarget {
// Loading state events
webContents.on('did-start-loading', () => {
this._lastError = undefined;
fireLoadingEvent(true);

// Don't fire loading events for e.g. same-document navigations
if (webContents.isLoadingMainFrame()) {
fireLoadingEvent(true);
}
});
webContents.on('did-stop-loading', () => fireLoadingEvent(false));
webContents.on('did-fail-load', (e, errorCode, errorDescription, validatedURL, isMainFrame) => {
Expand Down
25 changes: 25 additions & 0 deletions src/vs/platform/git/common/localGitService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { createDecorator } from '../../instantiation/common/instantiation.js';

export const ILocalGitService = createDecorator<ILocalGitService>('localGitService');

/**
* Low-level service for executing git commands on the local machine.
* Used in the shared process where Node.js APIs are available.
* All path arguments are native file-system paths.
*/
export interface ILocalGitService {
readonly _serviceBrand: undefined;

clone(operationId: string, cloneUrl: string, targetPath: string, ref?: string): Promise<void>;
pull(operationId: string, repoPath: string): Promise<boolean>;
checkout(operationId: string, repoPath: string, treeish: string, detached?: boolean): Promise<void>;
revParse(repoPath: string, ref: string): Promise<string>;
fetch(operationId: string, repoPath: string): Promise<void>;
revListCount(repoPath: string, fromRef: string, toRef: string): Promise<number>;
cancel(operationId: string): Promise<void>;
}
84 changes: 84 additions & 0 deletions src/vs/platform/git/node/localGitService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as cp from 'child_process';
import { CancellationError } from '../../../base/common/errors.js';
import { generateUuid } from '../../../base/common/uuid.js';
import { ILocalGitService } from '../common/localGitService.js';
import { ILogService } from '../../log/common/log.js';

export class LocalGitService implements ILocalGitService {
declare readonly _serviceBrand: undefined;

private _runningProcesses = new Map<string, cp.ChildProcess>();

constructor(
@ILogService private readonly _logService: ILogService,
) { }

private _exec(operationId: string, args: string[], cwd?: string): Promise<string> {
return new Promise((resolve, reject) => {
this._logService.trace(`[LocalGitService] git ${args.join(' ')}${cwd ? ` (cwd: ${cwd})` : ''}`);
const proc = cp.execFile('git', args, { cwd, encoding: 'utf8' }, (err, stdout, stderr) => {
if (!this._runningProcesses.delete(operationId)) {
reject(new CancellationError());
return;
}
if (err) {
this._logService.error(`[LocalGitService] git ${args[0]} failed:`, err.message, stderr);
reject(err);
return;
}
resolve(stdout);
});

this._runningProcesses.set(operationId, proc);
});
}

async clone(operationId: string, cloneUrl: string, targetPath: string, ref?: string): Promise<void> {
const args = ['clone'];
if (ref) {
args.push('--branch', ref);
}
args.push('--', cloneUrl, targetPath);
await this._exec(operationId, args);
}

async pull(operationId: string, repoPath: string): Promise<boolean> {
const before = (await this._exec(operationId, ['rev-parse', 'HEAD'], repoPath)).trim();
await this._exec(operationId, ['pull', '--ff-only'], repoPath);
const after = (await this._exec(operationId, ['rev-parse', 'HEAD'], repoPath)).trim();
return before !== after;
}

async checkout(operationId: string, repoPath: string, treeish: string, detached?: boolean): Promise<void> {
const args = detached
? ['checkout', '--detach', treeish]
: ['checkout', treeish];
await this._exec(operationId, args, repoPath);
}

async revParse(repoPath: string, ref: string): Promise<string> {
return (await this._exec(generateUuid(), ['rev-parse', ref], repoPath)).trim();
}

async fetch(operationId: string, repoPath: string): Promise<void> {
await this._exec(operationId, ['fetch'], repoPath);
}

async revListCount(repoPath: string, fromRef: string, toRef: string): Promise<number> {
const result = await this._exec(generateUuid(), ['rev-list', '--count', `${fromRef}..${toRef}`], repoPath);
return Number(result.trim()) || 0;
}

async cancel(operationId: string): Promise<void> {
const proc = this._runningProcesses.get(operationId);
if (proc) {
this._runningProcesses.delete(operationId);
proc.kill();
}
}
}
6 changes: 5 additions & 1 deletion src/vs/sessions/LAYOUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ The Agent Sessions titlebar includes a command center with a custom title bar wi

The widget:
- Extends `BaseActionViewItem` and renders a clickable label showing the active session title
- Shows kind icon (provider type icon), session title, repository folder name, and changes summary (+insertions -deletions)
- Shows kind icon (provider type icon), session title, repository folder name, and the active git branch/worktree name in parentheses when available, plus the changes summary (+insertions -deletions)
- On click, opens the `AgentSessionsPicker` quick pick to switch between sessions
- Gets the active session label from `IActiveSessionService.getActiveSession()` and the live model title from `IChatService`, falling back to "New Session" if no active session is found
- Re-renders automatically when the active session changes via `autorun` on `IActiveSessionService.activeSession`, and when session data changes via `IAgentSessionsService.model.onDidChangeSessions`
Expand Down Expand Up @@ -173,6 +173,8 @@ This structure places the sidebar at the root level spanning the full window hei
| Panel | 300px height |
| Titlebar | Determined by `minimumHeight` (~30px) |

The sessions sidebar can be resized down to a minimum width of 170px.

### 4.3 Editor Modal

The main editor part is created but hidden (`display:none`). It exists for future use but is not currently visible. All editors are forced to open in the `ModalEditorPart` overlay via the standard `createModalEditorPart()` mechanism.
Expand Down Expand Up @@ -644,6 +646,8 @@ interface IPartVisibilityState {

| Date | Change |
|------|--------|
| 2026-04-03 | Updated `SessionsTitleBarWidget` to format active session titles as `{Title} · {repo name} ({git branch/worktree name})` when repository detail metadata is available, falling back to the worktree folder name when needed. |
| 2026-04-03 | Reduced the sessions left sidebar minimum resizable width from 270px to 170px so it can shrink significantly more while keeping the default 300px width unchanged |
| 2026-03-30 | Adjusted `.agent-sessions-titlebar-container` padding so it sits flush when the sidebar is visible and restores 16px left padding when the sidebar is hidden |
| 2026-03-26 | Updated the sessions sidebar appear animation so only the body content (`.part.sidebar > .content`) slides/fades in during reveal while the sidebar title/header and footer remain fixed |
| 2026-03-24 | Polished the sessions task configuration quick input modal to use stronger modal-style header chrome, increased horizontal padding in the quick input/form content, and added an explicit close action in the modal header |
Expand Down
2 changes: 1 addition & 1 deletion src/vs/sessions/browser/parts/sidebarPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class SidebarPart extends AbstractPaneCompositePart {

//#region IView

readonly minimumWidth: number = 270;
readonly minimumWidth: number = 170;
readonly maximumWidth: number = Number.POSITIVE_INFINITY;
readonly minimumHeight: number = 0;
readonly maximumHeight: number = Number.POSITIVE_INFINITY;
Expand Down
Loading
Loading