Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
05e2a94
Add button to move sync terminals to background
Tyriar Jan 21, 2026
00da722
Fix button disappearing after using it
Tyriar Jan 21, 2026
1e1f4f5
Move button to the left
Tyriar Jan 21, 2026
540724f
Remove unneeded telemetry event
Tyriar Jan 21, 2026
68344f9
darker editor background, lighter panel background in 2026 light expe…
eli-w-king Jan 21, 2026
ef6d205
fixed gague colors
eli-w-king Jan 22, 2026
d3716eb
reverted css changes
eli-w-king Jan 22, 2026
5412f95
Merge branch 'main' into eli/theme-bg
eli-w-king Jan 22, 2026
e03d444
Git - store last opened time in the repository cache (#289612)
lszomoru Jan 22, 2026
25c32af
Git - mark `git.worktreeIncludeFiles` as experimental and reset the d…
lszomoru Jan 22, 2026
e00b7cd
Enhance Quick Fix action with inline chat integration (#289613)
jrieken Jan 22, 2026
665927c
agent sessions - never remove persisted sessions state (#289618)
bpasero Jan 22, 2026
c9dff6c
Initial plan
Copilot Jan 22, 2026
119f50a
Merge pull request #289558 from microsoft/eli/theme-bg
eli-w-king Jan 22, 2026
4293190
Fix leaked disposable in chat input by registering observe() return v…
Copilot Jan 22, 2026
0ae8610
style: update padding and margin for find widget buttons and adjust b…
mrleemurray Jan 22, 2026
d715a0b
Remove box-shadow and background styles from breadcrumbs control in e…
mrleemurray Jan 22, 2026
401c155
Chat - add new API to be able to represent a file that was deleted in…
lszomoru Jan 22, 2026
97c316e
Remember welcome view wide state (#286769)
alexr00 Jan 22, 2026
af5ec1e
Merge pull request #289626 from microsoft/mrleemurray/constitutional-…
mrleemurray Jan 22, 2026
8f72904
style: update disabledForeground color and enhance editor widget back…
mrleemurray Jan 22, 2026
075595e
fix: update placeholder text in InlineChatInputWidget based on select…
jrieken Jan 22, 2026
4680da4
Merge pull request #289631 from microsoft/mrleemurray/icy-gopher-pink
mrleemurray Jan 22, 2026
c43fa99
fix: enforce font size and color for unified send button icon
mrleemurray Jan 22, 2026
952749c
Merge pull request #289638 from microsoft/mrleemurray/parallel-possum…
mrleemurray Jan 22, 2026
e4bba07
Merge pull request #289639 from microsoft/mrleemurray/impossible-puma…
mrleemurray Jan 22, 2026
f9b7175
Remove redundancies, improve logging
Tyriar Jan 22, 2026
4074912
Register event smitters in TerminalChatService
Tyriar Jan 22, 2026
f7730c4
Update @vscode/proxy-agent to 0.37.0
chrmarti Jan 22, 2026
08d958a
Copy codicons file as part of the build (#289411)
alexr00 Jan 22, 2026
5345814
build(deps-dev): bump lodash from 4.17.21 to 4.17.23 in /test/monaco …
dependabot[bot] Jan 22, 2026
66c4801
build(deps-dev): bump lodash from 4.17.21 to 4.17.23 (#289515)
dependabot[bot] Jan 22, 2026
34a38fa
Merge pull request #289476 from microsoft/tyriar/261266
Tyriar Jan 22, 2026
e37fdc9
fix: move askpass scripts to stable location (#289400)
joaomoreno Jan 22, 2026
4ab2343
agent sessions - timings tweaks for short periods (#289661)
bpasero Jan 22, 2026
88fa474
Support overwrite updates on DarwinUpdateService (#288129)
joaomoreno Jan 22, 2026
a6e8518
Merge pull request #289622 from microsoft/copilot/fix-leaked-disposab…
jrieken Jan 22, 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 @@ -28,6 +28,7 @@ Visual Studio Code is built with a layered architecture using TypeScript, web AP
The core architecture follows these principles:
- **Layered architecture** - from `base`, `platform`, `editor`, to `workbench`
- **Dependency injection** - Services are injected through constructor parameters
- If non-service parameters are needed, they need to come after the service parameters
- **Contribution model** - Features contribute to registries and extension points
- **Cross-platform compatibility** - Abstractions separate platform-specific code

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ node_modules/
.build/
.vscode/extensions/**/out/
extensions/**/dist/
src/vs/base/browser/ui/codicons/codicon/codicon.ttf
/out*/
/extensions/**/out/
build/node_modules
Expand Down
8 changes: 8 additions & 0 deletions build/gulpfile.editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ const BUNDLED_FILE_HEADER = [
].join('\n');

const extractEditorSrcTask = task.define('extract-editor-src', () => {
// Ensure codicon.ttf is copied from node_modules (needed when node_modules is cached and postinstall doesn't run)
const codiconSource = path.join(root, 'node_modules', '@vscode', 'codicons', 'dist', 'codicon.ttf');
const codiconDest = path.join(root, 'src', 'vs', 'base', 'browser', 'ui', 'codicons', 'codicon', 'codicon.ttf');
if (fs.existsSync(codiconSource)) {
fs.mkdirSync(path.dirname(codiconDest), { recursive: true });
fs.copyFileSync(codiconSource, codiconDest);
}

const apiusages = monacoapi.execute().usageContent;
const extrausages = fs.readFileSync(path.join(root, 'build', 'monaco', 'monaco.usage.recipe')).toString();
standalone.extractEditor({
Expand Down
18 changes: 18 additions & 0 deletions build/npm/postinstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,21 @@ for (const dir of dirs) {

child_process.execSync('git config pull.rebase merges');
child_process.execSync('git config blame.ignoreRevsFile .git-blame-ignore-revs');

// Copy codicon.ttf from @vscode/codicons package
const codiconSource = path.join(root, 'node_modules', '@vscode', 'codicons', 'dist', 'codicon.ttf');
const codiconDest = path.join(root, 'src', 'vs', 'base', 'browser', 'ui', 'codicons', 'codicon', 'codicon.ttf');

if (!fs.existsSync(codiconSource)) {
console.error(`ERR codicon.ttf not found at ${codiconSource}`);
process.exit(1);
}

try {
fs.mkdirSync(path.dirname(codiconDest), { recursive: true });
fs.copyFileSync(codiconSource, codiconDest);
log('.', `Copied codicon.ttf to ${codiconDest}`);
} catch (error) {
console.error(`ERR Failed to copy codicon.ttf from ${codiconSource} to ${codiconDest}:`, error);
process.exit(1);
}
9 changes: 5 additions & 4 deletions extensions/git/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3592,11 +3592,12 @@
"items": {
"type": "string"
},
"default": [
"**/node_modules/**"
],
"default": [],
"markdownDescription": "%config.worktreeIncludeFiles%",
"scope": "resource",
"markdownDescription": "%config.worktreeIncludeFiles%"
"tags": [
"experimental"
]
},
"git.alwaysShowStagedChangesResourceGroup": {
"type": "boolean",
Expand Down
17 changes: 12 additions & 5 deletions extensions/git/src/askpass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

import { window, InputBoxOptions, Uri, Disposable, workspace, QuickPickOptions, l10n, LogOutputChannel } from 'vscode';
import { IDisposable, EmptyDisposable, toDisposable, extractFilePathFromArgs } from './util';
import * as path from 'path';
import { IIPCHandler, IIPCServer } from './ipc/ipcServer';
import { CredentialsProvider, Credentials } from './api/git';
import { ITerminalEnvironmentProvider } from './terminal';
import { AskpassPaths } from './askpassManager';

export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider {

Expand All @@ -20,23 +20,30 @@ export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider {

readonly featureDescription = 'git auth provider';

constructor(private ipc: IIPCServer | undefined, private readonly logger: LogOutputChannel) {
constructor(
private ipc: IIPCServer | undefined,
private readonly logger: LogOutputChannel,
askpassPaths: AskpassPaths
) {
if (ipc) {
this.disposable = ipc.registerHandler('askpass', this);
}

const askpassScript = this.ipc ? askpassPaths.askpass : askpassPaths.askpassEmpty;
const sshAskpassScript = this.ipc ? askpassPaths.sshAskpass : askpassPaths.sshAskpassEmpty;

this.env = {
// GIT_ASKPASS
GIT_ASKPASS: path.join(__dirname, this.ipc ? 'askpass.sh' : 'askpass-empty.sh'),
GIT_ASKPASS: askpassScript,
// VSCODE_GIT_ASKPASS
VSCODE_GIT_ASKPASS_NODE: process.execPath,
VSCODE_GIT_ASKPASS_EXTRA_ARGS: '',
VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js')
VSCODE_GIT_ASKPASS_MAIN: askpassPaths.askpassMain
};

this.sshEnv = {
// SSH_ASKPASS
SSH_ASKPASS: path.join(__dirname, this.ipc ? 'ssh-askpass.sh' : 'ssh-askpass-empty.sh'),
SSH_ASKPASS: sshAskpassScript,
SSH_ASKPASS_REQUIRE: 'force'
};
}
Expand Down
233 changes: 233 additions & 0 deletions extensions/git/src/askpassManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import * as cp from 'child_process';
import { env, LogOutputChannel } from 'vscode';

/**
* Manages content-addressed copies of askpass scripts in a user-controlled folder.
*
* This solves the problem on Windows user/system setups where environment variables
* like GIT_ASKPASS point to scripts inside the VS Code installation directory, which
* changes on each update. By copying the scripts to a content-addressed location in
* user storage, the paths remain stable across updates (as long as the script contents
* don't change).
*
* This feature is only enabled on Windows user and system setups (not archive or portable)
* because those are the only configurations where the installation path changes on each update.
*
* Security considerations:
* - Scripts are placed in user-controlled storage (not TEMP to avoid TOCTOU attacks)
* - On Windows, ACLs are set to allow only the current user to modify the files
*/

/**
* Checks if the current VS Code installation is a Windows user or system setup.
* Returns false for archive, portable, or non-Windows installations.
*/
function isWindowsUserOrSystemSetup(): boolean {
if (process.platform !== 'win32') {
return false;
}

try {
const productJsonPath = path.join(env.appRoot, 'product.json');
const productJson = JSON.parse(fs.readFileSync(productJsonPath, 'utf8'));
const target = productJson.target as string | undefined;

// Target is 'user' or 'system' for Inno Setup installations.
// Archive and portable builds don't have a target property.
return target === 'user' || target === 'system';
} catch {
// If we can't read product.json, assume not applicable
return false;
}
}

interface SourceAskpassPaths {
askpass: string;
askpassMain: string;
sshAskpass: string;
askpassEmpty: string;
sshAskpassEmpty: string;
}

/**
* Computes a SHA-256 hash of the combined contents of all askpass-related files.
* This hash is used to create content-addressed directories.
*/
function computeContentHash(sourcePaths: SourceAskpassPaths): string {
const hash = crypto.createHash('sha256');

// Hash all source files in a deterministic order
const files = [
sourcePaths.askpass,
sourcePaths.askpassMain,
sourcePaths.sshAskpass,
sourcePaths.askpassEmpty,
sourcePaths.sshAskpassEmpty,
];

for (const file of files) {
const content = fs.readFileSync(file);
hash.update(content);
// Include filename in hash to ensure different files with same content produce different hash
hash.update(path.basename(file));
}

return hash.digest('hex').substring(0, 16);
}

/**
* Sets restrictive file permissions on Windows using icacls.
* Grants full control only to the current user and removes inherited permissions.
*/
async function setWindowsPermissions(filePath: string, logger: LogOutputChannel): Promise<void> {
const username = process.env['USERNAME'];
if (!username) {
logger.warn(`[askpassManager] Cannot set Windows permissions: USERNAME not set`);
return;
}

return new Promise<void>((resolve) => {
// icacls <file> /inheritance:r /grant:r "<username>:F"
// /inheritance:r - Remove all inherited permissions
// /grant:r - Replace (not add) permissions, giving Full control to user
const args = [filePath, '/inheritance:r', '/grant:r', `${username}:F`];

cp.execFile('icacls', args, (error, _stdout, stderr) => {
if (error) {
logger.warn(`[askpassManager] Failed to set permissions on ${filePath}: ${error.message}`);
if (stderr) {
logger.warn(`[askpassManager] icacls stderr: ${stderr}`);
}
} else {
logger.trace(`[askpassManager] Set permissions on ${filePath}`);
}
resolve();
});
});
}

/**
* Copies a file to the destination, creating parent directories as needed.
* Sets restrictive permissions on the copied file.
*/
async function copyFileSecure(
source: string,
dest: string,
logger: LogOutputChannel
): Promise<void> {
const content = await fs.promises.readFile(source);
await fs.promises.writeFile(dest, content);
await setWindowsPermissions(dest, logger);
}

export interface AskpassPaths {
readonly askpass: string;
readonly askpassMain: string;
readonly sshAskpass: string;
readonly askpassEmpty: string;
readonly sshAskpassEmpty: string;
}

/**
* Ensures that content-addressed copies of askpass scripts exist in user storage.
* Returns the paths to the content-addressed copies.
*
* @param sourceDir The directory containing the original askpass scripts (__dirname)
* @param storageDir The user-controlled storage directory (context.storageUri.fsPath)
* @param logger Logger for diagnostic output
*/
async function ensureAskpassScripts(
sourceDir: string,
storageDir: string,
logger: LogOutputChannel
): Promise<AskpassPaths> {
const sourcePaths: SourceAskpassPaths = {
askpass: path.join(sourceDir, 'askpass.sh'),
askpassMain: path.join(sourceDir, 'askpass-main.js'),
sshAskpass: path.join(sourceDir, 'ssh-askpass.sh'),
askpassEmpty: path.join(sourceDir, 'askpass-empty.sh'),
sshAskpassEmpty: path.join(sourceDir, 'ssh-askpass-empty.sh'),
};

// Compute content hash
const contentHash = computeContentHash(sourcePaths);
logger.trace(`[askpassManager] Content hash: ${contentHash}`);

// Create content-addressed directory
const askpassDir = path.join(storageDir, 'askpass', contentHash);

const destPaths: AskpassPaths = {
askpass: path.join(askpassDir, 'askpass.sh'),
askpassMain: path.join(askpassDir, 'askpass-main.js'),
sshAskpass: path.join(askpassDir, 'ssh-askpass.sh'),
askpassEmpty: path.join(askpassDir, 'askpass-empty.sh'),
sshAskpassEmpty: path.join(askpassDir, 'ssh-askpass-empty.sh'),
};

// Check if already exists (fast path for subsequent activations)
try {
const stat = await fs.promises.stat(destPaths.askpass);
if (stat.isFile()) {
logger.trace(`[askpassManager] Using existing content-addressed askpass at ${askpassDir}`);
return destPaths;
}
} catch {
// Directory doesn't exist, create it
}

logger.info(`[askpassManager] Creating content-addressed askpass scripts at ${askpassDir}`);

// Create directory and set Windows ACLs
await fs.promises.mkdir(askpassDir, { recursive: true });
await setWindowsPermissions(askpassDir, logger);

// Copy all files
await Promise.all([
copyFileSecure(sourcePaths.askpass, destPaths.askpass, logger),
copyFileSecure(sourcePaths.askpassMain, destPaths.askpassMain, logger),
copyFileSecure(sourcePaths.sshAskpass, destPaths.sshAskpass, logger),
copyFileSecure(sourcePaths.askpassEmpty, destPaths.askpassEmpty, logger),
copyFileSecure(sourcePaths.sshAskpassEmpty, destPaths.sshAskpassEmpty, logger),
]);

logger.info(`[askpassManager] Successfully created content-addressed askpass scripts`);

return destPaths;
}

/**
* Returns the askpass script paths. Uses content-addressed copies
* on Windows user/system setups (to keep paths stable across updates),
* otherwise returns paths relative to the source directory.
*/
export async function getAskpassPaths(
sourceDir: string,
storagePath: string | undefined,
logger: LogOutputChannel
): Promise<AskpassPaths> {
// Try content-addressed paths on Windows user/system setups
if (storagePath && isWindowsUserOrSystemSetup()) {
try {
return await ensureAskpassScripts(sourceDir, storagePath, logger);
} catch (err) {
logger.error(`[askpassManager] Failed to create content-addressed askpass scripts: ${err}`);
}
}

// Fallback to source directory paths (for development or non-Windows setups)
return {
askpass: path.join(sourceDir, 'askpass.sh'),
askpassMain: path.join(sourceDir, 'askpass-main.js'),
sshAskpass: path.join(sourceDir, 'ssh-askpass.sh'),
askpassEmpty: path.join(sourceDir, 'askpass-empty.sh'),
sshAskpassEmpty: path.join(sourceDir, 'ssh-askpass-empty.sh'),
};
}
4 changes: 3 additions & 1 deletion extensions/git/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { GitEditSessionIdentityProvider } from './editSessionIdentityProvider';
import { GitCommitInputBoxCodeActionsProvider, GitCommitInputBoxDiagnosticsManager } from './diagnostics';
import { GitBlameController } from './blame';
import { CloneManager } from './cloneManager';
import { getAskpassPaths } from './askpassManager';

const deactivateTasks: { (): Promise<void> }[] = [];

Expand Down Expand Up @@ -71,7 +72,8 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel,
logger.error(`[main] Failed to create git IPC: ${err}`);
}

const askpass = new Askpass(ipcServer, logger);
const askpassPaths = await getAskpassPaths(__dirname, context.globalStorageUri.fsPath, logger);
const askpass = new Askpass(ipcServer, logger, askpassPaths);
disposables.push(askpass);

const gitEditor = new GitEditor(ipcServer);
Expand Down
2 changes: 1 addition & 1 deletion extensions/git/src/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1902,7 +1902,7 @@ export class Repository implements Disposable {

private async _getWorktreeIncludePaths(): Promise<Set<string>> {
const config = workspace.getConfiguration('git', Uri.file(this.root));
const worktreeIncludeFiles = config.get<string[]>('worktreeIncludeFiles', ['**/node_modules/**']);
const worktreeIncludeFiles = config.get<string[]>('worktreeIncludeFiles', []);

if (worktreeIncludeFiles.length === 0) {
return new Set<string>();
Expand Down
Loading
Loading