Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
155c18e
updates component explorer (#296938)
hediet Feb 23, 2026
6b820e7
Add diagnostics support to inline chat and related actions (#296939)
jrieken Feb 23, 2026
bdd6e80
update screenshot baselines from CI
hediet Feb 23, 2026
e50a372
Post url to review screenshots
hediet Feb 23, 2026
70bbf7a
fix: update position icon in notifications to use arrowSwap
mrleemurray Feb 23, 2026
9c2922a
feat: enhance source map handling in NLS and private field conversion…
jrieken Feb 23, 2026
234d552
Use static product info for Show Release Notes command (#296708)
dmitrivMS Feb 23, 2026
f8f66ef
Remove bottom border from chat tip widget for improved styling
mrleemurray Feb 23, 2026
89cd7ec
adds screenshot status with url, updates skill
hediet Feb 23, 2026
8b99d0c
Merge pull request #296969 from microsoft/mrleemurray/ugliest-scarlet…
mrleemurray Feb 23, 2026
652ffe5
Merge pull request #296971 from microsoft/mrleemurray/depressed-rose-…
mrleemurray Feb 23, 2026
a4098a4
feedback (#296979)
sandy081 Feb 23, 2026
66c03bd
handle worktrees in folder picker (#296981)
sandy081 Feb 23, 2026
059d431
improve opening new session (#296977)
sandy081 Feb 23, 2026
a609cbd
sessions - support to open a session in a protocol link opened window…
bpasero Feb 23, 2026
68703b5
Agentic browser (#296665)
kycutler Feb 23, 2026
0bafbc1
Bump hono from 4.11.7 to 4.12.0 in /test/mcp (#296460)
dependabot[bot] Feb 23, 2026
a35852c
Bump lodash-es and mermaid in /extensions/mermaid-chat-features (#296…
dependabot[bot] Feb 23, 2026
022061d
Bump minimatch from 10.0.3 to 10.2.2 in /extensions/css-language-feat…
dependabot[bot] Feb 23, 2026
845a614
feat - allow to use all workbench commands in sessions window from mo…
bpasero Feb 23, 2026
72f8999
modal - increase minimal size (#296984)
bpasero Feb 23, 2026
6c8628d
Disable unregistered session types in session type picker (#296974)
Copilot Feb 23, 2026
4eb8565
Get accurate Windows version info from registry instead of os.release…
dmitrivMS Feb 23, 2026
b482c86
chat - update instructions (#296995)
bpasero Feb 23, 2026
28056c3
Revert "Raise keybinding precedence for chat question carousel and co…
meganrogge Feb 23, 2026
9bd2941
Update @vscode/codicons to version 0.0.45-10 in package.json and pack…
mrleemurray Feb 23, 2026
9d4ae34
Merge pull request #297012 from microsoft/mrleemurray/bottom-white-shark
mrleemurray Feb 23, 2026
2650c90
use git lfs (#297013)
hediet Feb 23, 2026
4273cad
add promptFilePickers.fixture.ts (#297005)
aeschli Feb 23, 2026
db8faee
Show `/create-*` chat tip only in local sessions (#297016)
Copilot Feb 23, 2026
d311bc9
Remove cli.js argument when running as administrator on Windows (#296…
dmitrivMS Feb 23, 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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ ThirdPartyNotices.txt eol=crlf
*.sh eol=lf
*.rtf -text
**/*.json linguist-language=jsonc

test/componentFixtures/.screenshots/**/*.png filter=lfs diff=lfs merge=lfs -text
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ function f(x: number, y: string): void { }
- You MUST deal with disposables by registering them immediately after creation for later disposal. Use helpers such as `DisposableStore`, `MutableDisposable` or `DisposableMap`. Do NOT register a disposable to the containing class if the object is created within a method that is called repeadedly to avoid leaks. Instead, return a `IDisposable` from such method and let the caller register it.
- 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.

## 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.
12 changes: 6 additions & 6 deletions .github/skills/update-screenshots/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,25 @@ Pick the most recent run that has a `screenshot-diff` artifact (runs where scree
gh run download <run-id> --name screenshot-diff --dir .tmp/screenshot-diff
```

This downloads:
- `test/componentFixtures/.screenshots/current/` — the CI-captured screenshots
- `test/componentFixtures/.screenshots/report.json` — structured diff report
- `test/componentFixtures/.screenshots/report.md` — human-readable diff report
The artifact is uploaded from two paths (`test/componentFixtures/.screenshots/current/` and `test/componentFixtures/.screenshots/report/`), but GitHub Actions strips the common prefix. So the downloaded structure is:
- `current/` — the CI-captured screenshots (e.g. `current/baseUI/Buttons/Dark.png`)
- `report/report.json` — structured diff report
- `report/report.md` — human-readable diff report

### 3. Review the changes

Show the user what changed by reading the markdown report:

```bash
cat .tmp/screenshot-diff/test/componentFixtures/.screenshots/report.md
cat .tmp/screenshot-diff/report/report.md
```

### 4. Copy CI screenshots to baseline

```bash
# Remove old baselines and replace with CI screenshots
rm -rf test/componentFixtures/.screenshots/baseline/
cp -r .tmp/screenshot-diff/test/componentFixtures/.screenshots/current/ test/componentFixtures/.screenshots/baseline/
cp -r .tmp/screenshot-diff/current/ test/componentFixtures/.screenshots/baseline/
```

### 5. Clean up
Expand Down
45 changes: 37 additions & 8 deletions .github/workflows/screenshot-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ permissions:
contents: read
pull-requests: write
checks: write
statuses: write

concurrency:
group: screenshots-${{ github.event.pull_request.number || github.sha }}
Expand All @@ -36,10 +37,18 @@ jobs:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Install build dependencies
run: npm ci
working-directory: build

- name: Install build/vite dependencies
run: rm -f package-lock.json && npm install
working-directory: build/vite

- name: Build vite
run: npm run build
working-directory: build/vite

- name: Install Playwright Chromium
run: npx playwright install chromium

Expand All @@ -51,25 +60,37 @@ jobs:
run: |
npx component-explorer screenshot:compare \
--project ./test/componentFixtures \
--report ./test/componentFixtures/.screenshots/report.json \
--report-markdown ./test/componentFixtures/.screenshots/report.md
--report ./test/componentFixtures/.screenshots/report
continue-on-error: true

- name: Prepare explorer artifact
run: |
mkdir -p /tmp/explorer-artifact/screenshot-report
cp -r build/vite/dist/* /tmp/explorer-artifact/
if [ -d test/componentFixtures/.screenshots/report ]; then
cp -r test/componentFixtures/.screenshots/report/* /tmp/explorer-artifact/screenshot-report/
fi

- name: Upload explorer artifact
uses: actions/upload-artifact@v4
with:
name: component-explorer
path: /tmp/explorer-artifact/

- name: Upload screenshot report
if: steps.compare.outcome == 'failure'
uses: actions/upload-artifact@v4
with:
name: screenshot-diff
path: |
test/componentFixtures/.screenshots/current/
test/componentFixtures/.screenshots/report.json
test/componentFixtures/.screenshots/report.md
test/componentFixtures/.screenshots/report/

- name: Set check title
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
REPORT="test/componentFixtures/.screenshots/report.json"
REPORT="test/componentFixtures/.screenshots/report/report.json"
if [ -f "$REPORT" ]; then
CHANGED=$(node -e "const r = require('./$REPORT'); console.log(r.summary.added + r.summary.removed + r.summary.changed)")
TITLE="${CHANGED} screenshots changed"
Expand All @@ -81,17 +102,25 @@ jobs:
CHECK_RUN_ID=$(gh api "repos/${{ github.repository }}/commits/$SHA/check-runs" \
--jq '.check_runs[] | select(.name == "screenshots") | .id')

DETAILS_URL="https://hediet-ghartifactpreview.azurewebsites.net/${{ github.repository }}/run/${{ github.run_id }}/component-explorer/___explorer.html?report=./screenshot-report/report.json"

if [ -n "$CHECK_RUN_ID" ]; then
gh api "repos/${{ github.repository }}/check-runs/$CHECK_RUN_ID" \
-X PATCH --input - <<EOF
{"output":{"title":"$TITLE","summary":"$TITLE"}}
{"details_url":"$DETAILS_URL","output":{"title":"$TITLE","summary":"$TITLE"}}
EOF
fi

DETAILS_URL="https://hediet-ghartifactpreview.azurewebsites.net/${{ github.repository }}/run/${{ github.run_id }}/component-explorer/___explorer.html?report=./screenshot-report/report.json"
gh api "repos/${{ github.repository }}/statuses/$SHA" \
--input - <<EOF
{"state":"success","target_url":"$DETAILS_URL","description":"$TITLE","context":"screenshots / explorer"}
EOF

- name: Post summary
run: |
if [ -f test/componentFixtures/.screenshots/report.md ]; then
cat test/componentFixtures/.screenshots/report.md >> $GITHUB_STEP_SUMMARY
if [ -f test/componentFixtures/.screenshots/report/report.md ]; then
cat test/componentFixtures/.screenshots/report/report.md >> $GITHUB_STEP_SUMMARY
else
echo "## Screenshots ✅" >> $GITHUB_STEP_SUMMARY
echo "No visual changes detected." >> $GITHUB_STEP_SUMMARY
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ vscode-telemetry-docs/
test-output.json
test/componentFixtures/.screenshots/*
!test/componentFixtures/.screenshots/baseline/
dist
33 changes: 25 additions & 8 deletions build/next/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { promisify } from 'util';
import glob from 'glob';
import gulpWatch from '../lib/watch/index.ts';
import { nlsPlugin, createNLSCollector, finalizeNLS, postProcessNLS } from './nls-plugin.ts';
import { convertPrivateFields, type ConvertPrivateFieldsResult } from './private-to-property.ts';
import { convertPrivateFields, adjustSourceMap, type ConvertPrivateFieldsResult } from './private-to-property.ts';
import { getVersion } from '../lib/getVersion.ts';
import product from '../../product.json' with { type: 'json' };
import packageJson from '../../package.json' with { type: 'json' };
Expand Down Expand Up @@ -883,6 +883,8 @@ ${tslib}`,
// Post-process and write all output files
let bundled = 0;
const mangleStats: { file: string; result: ConvertPrivateFieldsResult }[] = [];
// Map from JS file path to pre-mangle content + edits, for source map adjustment
const mangleEdits = new Map<string, { preMangleCode: string; edits: readonly import('./private-to-property.ts').TextEdit[] }>();
for (const { result } of buildResults) {
if (!result.outputFiles) {
continue;
Expand All @@ -894,22 +896,26 @@ ${tslib}`,
if (file.path.endsWith('.js') || file.path.endsWith('.css')) {
let content = file.text;

// Apply NLS post-processing if enabled (JS only)
if (file.path.endsWith('.js') && doNls && indexMap.size > 0) {
content = postProcessNLS(content, indexMap, preserveEnglish);
}

// Convert native #private fields to regular properties.
// Convert native #private fields to regular properties BEFORE NLS
// post-processing, so that the edit offsets align with esbuild's
// source map coordinate system (both reference the raw esbuild output).
// Skip extension host bundles - they expose API surface to extensions
// where true encapsulation matters more than the perf gain.
if (file.path.endsWith('.js') && doManglePrivates && !isExtensionHostBundle(file.path)) {
const preMangleCode = content;
const mangleResult = convertPrivateFields(content, file.path);
content = mangleResult.code;
if (mangleResult.editCount > 0) {
mangleStats.push({ file: path.relative(path.join(REPO_ROOT, outDir), file.path), result: mangleResult });
mangleEdits.set(file.path, { preMangleCode, edits: mangleResult.edits });
}
}

// Apply NLS post-processing if enabled (JS only)
if (file.path.endsWith('.js') && doNls && indexMap.size > 0) {
content = postProcessNLS(content, indexMap, preserveEnglish);
}

// Rewrite sourceMappingURL to CDN URL if configured
if (sourceMapBaseUrl) {
const relativePath = path.relative(path.join(REPO_ROOT, outDir), file.path);
Expand All @@ -924,8 +930,19 @@ ${tslib}`,
}

await fs.promises.writeFile(file.path, content);
} else if (file.path.endsWith('.map')) {
// Source maps may need adjustment if private fields were mangled
const jsPath = file.path.replace(/\.map$/, '');
const editInfo = mangleEdits.get(jsPath);
if (editInfo) {
const mapJson = JSON.parse(file.text);
const adjusted = adjustSourceMap(mapJson, editInfo.preMangleCode, editInfo.edits);
await fs.promises.writeFile(file.path, JSON.stringify(adjusted));
} else {
await fs.promises.writeFile(file.path, file.contents);
}
} else {
// Write other files (source maps, assets) as-is
// Write other files (assets, etc.) as-is
await fs.promises.writeFile(file.path, file.contents);
}
}
Expand Down
109 changes: 102 additions & 7 deletions build/next/nls-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import * as esbuild from 'esbuild';
import * as path from 'path';
import * as fs from 'fs';
import { SourceMapGenerator } from 'source-map';
import {
TextModel,
analyzeLocalizeCalls,
Expand Down Expand Up @@ -160,10 +161,17 @@ export function postProcessNLS(
// Transformation
// ============================================================================

interface NLSEdit {
line: number; // 0-based line in original source
startCol: number; // 0-based start column in original
endCol: number; // 0-based end column in original
newLength: number; // length of replacement text
}

function transformToPlaceholders(
source: string,
moduleId: string
): { code: string; entries: NLSEntry[] } {
): { code: string; entries: NLSEntry[]; edits: NLSEdit[] } {
const localizeCalls = analyzeLocalizeCalls(source, 'localize');
const localize2Calls = analyzeLocalizeCalls(source, 'localize2');

Expand All @@ -176,10 +184,11 @@ function transformToPlaceholders(
);

if (allCalls.length === 0) {
return { code: source, entries: [] };
return { code: source, entries: [], edits: [] };
}

const entries: NLSEntry[] = [];
const edits: NLSEdit[] = [];
const model = new TextModel(source);

// Process in reverse order to preserve positions
Expand All @@ -201,14 +210,92 @@ function transformToPlaceholders(
placeholder
});

const replacementText = `"${placeholder}"`;

// Track the edit for source map generation (positions are in original source coords)
edits.push({
line: call.keySpan.start.line,
startCol: call.keySpan.start.character,
endCol: call.keySpan.end.character,
newLength: replacementText.length,
});

// Replace the key with the placeholder string
model.apply(call.keySpan, `"${placeholder}"`);
model.apply(call.keySpan, replacementText);
}

// Reverse entries to match source order
// Reverse entries and edits to match source order
entries.reverse();
edits.reverse();

return { code: model.toString(), entries, edits };
}

/**
* Generates a source map that maps from the NLS-transformed source back to the
* original source. esbuild composes this with its own bundle source map so that
* the final source map points all the way back to the untransformed TypeScript.
*/
function generateNLSSourceMap(
originalSource: string,
filePath: string,
edits: NLSEdit[]
): string {
const generator = new SourceMapGenerator();
generator.setSourceContent(filePath, originalSource);

const lineCount = originalSource.split('\n').length;

// Group edits by line
const editsByLine = new Map<number, NLSEdit[]>();
for (const edit of edits) {
let arr = editsByLine.get(edit.line);
if (!arr) {
arr = [];
editsByLine.set(edit.line, arr);
}
arr.push(edit);
}

for (let line = 0; line < lineCount; line++) {
const smLine = line + 1; // source maps use 1-based lines

// Always map start of line
generator.addMapping({
generated: { line: smLine, column: 0 },
original: { line: smLine, column: 0 },
source: filePath,
});

const lineEdits = editsByLine.get(line);
if (lineEdits) {
lineEdits.sort((a, b) => a.startCol - b.startCol);

let cumulativeShift = 0;

for (const edit of lineEdits) {
const origLen = edit.endCol - edit.startCol;

// Map start of edit: the replacement begins at the same original position
generator.addMapping({
generated: { line: smLine, column: edit.startCol + cumulativeShift },
original: { line: smLine, column: edit.startCol },
source: filePath,
});

cumulativeShift += edit.newLength - origLen;

// Map content after edit: columns resume with the shift applied
generator.addMapping({
generated: { line: smLine, column: edit.endCol + cumulativeShift },
original: { line: smLine, column: edit.endCol },
source: filePath,
});
}
}
}

return { code: model.toString(), entries };
return generator.toString();
}

function replaceInOutput(
Expand Down Expand Up @@ -300,15 +387,23 @@ export function nlsPlugin(options: NLSPluginOptions): esbuild.Plugin {
.replace(/\.ts$/, '');

// Transform localize() calls to placeholders
const { code, entries: fileEntries } = transformToPlaceholders(source, moduleId);
const { code, entries: fileEntries, edits } = transformToPlaceholders(source, moduleId);

// Collect entries
for (const entry of fileEntries) {
collector.add(entry);
}

if (fileEntries.length > 0) {
return { contents: code, loader: 'ts' };
// Generate a source map that maps from the NLS-transformed source
// back to the original. Embed it inline so esbuild composes it
// with its own bundle source map, making the final map point to
// the original TS source.
const sourceName = relativePath.replace(/\\/g, '/');
const sourcemap = generateNLSSourceMap(source, sourceName, edits);
const encodedMap = Buffer.from(sourcemap).toString('base64');
const contentsWithMap = code + `\n//# sourceMappingURL=data:application/json;base64,${encodedMap}\n`;
return { contents: contentsWithMap, loader: 'ts' };
}

// No NLS calls, return undefined to let esbuild handle normally
Expand Down
Loading
Loading