Merge branch 'main' into dev/mjbvz/foolish-quelea

This commit is contained in:
Matt Bierner
2026-02-18 09:13:50 -08:00
192 changed files with 8109 additions and 3605 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
disturl="https://electronjs.org/headers"
target="39.6.0"
ms_build_id="13312042"
ms_build_id="13330601"
runtime="electron"
ignore-scripts=false
build_from_source="true"
+1 -1
View File
@@ -70,7 +70,7 @@
"test/smoke/out/**": true,
"test/automation/out/**": true,
"test/integration/browser/out/**": true,
"src/vs/sessions/**": true
// "src/vs/sessions/**": true
},
// --- Search ---
"search.exclude": {
+3 -2
View File
@@ -73,10 +73,11 @@
"owner": "typescript",
"applyTo": "closedDocuments",
"fileLocation": [
"absolute"
"relative",
"${workspaceFolder}"
],
"pattern": {
"regexp": "Error: ([^(]+)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\): (.*)$",
"regexp": "\\] ([^(]+)\\((\\d+,\\d+)\\): (.*)$",
"file": 1,
"location": 2,
"message": 3
+39 -1
View File
@@ -8,6 +8,7 @@ import { EventEmitter } from 'events';
EventEmitter.defaultMaxListeners = 100;
import es from 'event-stream';
import fancyLog from 'fancy-log';
import glob from 'glob';
import gulp from 'gulp';
import filter from 'gulp-filter';
@@ -27,6 +28,25 @@ import watcher from './lib/watch/index.ts';
const root = path.dirname(import.meta.dirname);
const commit = getVersion(root);
// Tracks active extension compilations to emit aggregate
// "Starting compilation" / "Finished compilation" messages
// that the problem matcher in tasks.json relies on.
let activeExtensionCompilations = 0;
function onExtensionCompilationStart(): void {
if (activeExtensionCompilations === 0) {
fancyLog('Starting compilation');
}
activeExtensionCompilations++;
}
function onExtensionCompilationEnd(): void {
activeExtensionCompilations--;
if (activeExtensionCompilations === 0) {
fancyLog('Finished compilation');
}
}
// To save 250ms for each gulp startup, we are caching the result here
// const compilations = glob.sync('**/tsconfig.json', {
// cwd: extensionsPath,
@@ -175,7 +195,25 @@ const tasks = compilations.map(function (tsconfigFile) {
const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'], { dot: true }));
const watchInput = watcher(src, { ...srcOpts, ...{ readDelay: 200 } });
const watchNonTs = watchInput.pipe(filter(['**', '!**/*.ts'], { dot: true })).pipe(gulp.dest(out));
const tsgoStream = watchInput.pipe(util.debounce(() => createTsgoStream(absolutePath, { taskName: 'extensions' }, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl)), 200));
const tsgoStream = watchInput.pipe(util.debounce(() => {
onExtensionCompilationStart();
const stream = createTsgoStream(absolutePath, { taskName: 'extensions' }, () => rewriteTsgoSourceMappingUrlsIfNeeded(false, out, baseUrl));
// Wrap in a result stream that always emits 'end' (even on
// error) so the debounce resets to idle and can process future
// file changes. Errors from tsgo (e.g. type errors causing a
// non-zero exit code) are already reported by spawnTsgo's
// runReporter, so swallowing the stream error is safe.
const result = es.through();
stream.on('end', () => {
onExtensionCompilationEnd();
result.emit('end');
});
stream.on('error', () => {
onExtensionCompilationEnd();
result.emit('end');
});
return result;
}, 200));
const watchStream = es.merge(nonts.pipe(gulp.dest(out)), watchNonTs, tsgoStream);
return watchStream;
+11 -1
View File
@@ -368,8 +368,18 @@ function generateExtensionPointNames() {
}, function () {
collectedNames.sort();
const content = JSON.stringify(collectedNames, undefined, '\t') + '\n';
const filePath = 'vs/workbench/services/extensions/common/extensionPoints.json';
try {
const existing = fs.readFileSync(path.join('src', filePath), 'utf-8');
if (existing.replace(/\r\n/g, '\n') === content) {
this.emit('end');
return;
}
} catch {
// File doesn't exist yet, emit it
}
this.emit('data', new File({
path: 'vs/workbench/services/extensions/common/extensionPoints.json',
path: filePath,
contents: Buffer.from(content)
}));
this.emit('end');
+6 -6
View File
@@ -441,7 +441,7 @@ interface IExtensionManifest {
/**
* Loosely based on `getExtensionKind` from `src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts`
*/
function isWebExtension(manifest: IExtensionManifest): boolean {
export function isWebExtension(manifest: IExtensionManifest): boolean {
if (Boolean(manifest.browser)) {
return true;
}
@@ -578,11 +578,11 @@ export function packageMarketplaceExtensionsStream(forWeb: boolean): Stream {
}
export interface IScannedBuiltinExtension {
extensionPath: string;
packageJSON: any;
packageNLS?: any;
readmePath?: string;
changelogPath?: string;
readonly extensionPath: string;
readonly packageJSON: unknown;
readonly packageNLS: unknown | undefined;
readonly readmePath: string | undefined;
readonly changelogPath: string | undefined;
}
export function scanBuiltinExtensions(extensionsRoot: string, exclude: string[] = []): IScannedBuiltinExtension[] {
+13
View File
@@ -211,10 +211,23 @@ function scanDirectory(dir: string): string[] {
return names;
}
function normalize(s: string): string {
return s.replace(/\r\n/g, '\n');
}
function main(): void {
const names = scanDirectory(path.join(srcDir, 'vs', 'workbench'));
names.sort();
const output = JSON.stringify(names, undefined, '\t') + '\n';
try {
const existing = fs.readFileSync(outputPath, 'utf-8');
if (normalize(existing) === normalize(output)) {
console.log(`No changes to ${path.relative(rootDir, outputPath)}`);
return;
}
} catch {
// File doesn't exist yet, write it
}
fs.writeFileSync(outputPath, output, 'utf-8');
console.log(`Wrote ${names.length} extension points to ${path.relative(rootDir, outputPath)}`);
}
+27 -16
View File
@@ -12,13 +12,17 @@ import * as path from 'path';
const root = path.dirname(path.dirname(import.meta.dirname));
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
const timestampRegex = /^\[\d{2}:\d{2}:\d{2}\]\s*/;
export function spawnTsgo(projectPath: string, config: { taskName: string; noEmit?: boolean }, onComplete?: () => Promise<void> | void): Promise<void> {
function runReporter(stdError: string) {
const matches = (stdError || '').match(/error \w+: (.+)?/g);
fancyLog(`Finished ${ansiColors.green(config.taskName)} ${projectPath} with ${matches ? matches.length : 0} errors.`);
for (const match of matches || []) {
fancyLog.error(match);
function runReporter(output: string) {
const lines = (output || '').split('\n');
const errorLines = lines.filter(line => /error \w+:/.test(line));
if (errorLines.length > 0) {
fancyLog(`Finished ${ansiColors.green(config.taskName)} ${projectPath} with ${errorLines.length} errors.`);
for (const line of errorLines) {
fancyLog(line);
}
}
}
@@ -34,21 +38,28 @@ export function spawnTsgo(projectPath: string, config: { taskName: string; noEmi
shell: true
});
const handleData = (data: Buffer) => {
const lines = data.toString()
.split(/\r?\n/)
.map(line => line.replace(ansiRegex, '').trim())
.filter(line => line.length > 0)
.filter(line => !/Starting compilation|File change detected|Compilation complete/i.test(line));
let stdoutData = '';
let stderrData = '';
runReporter(lines.join('\n'));
};
child.stdout?.on('data', handleData);
child.stderr?.on('data', handleData);
child.stdout?.on('data', (data: Buffer) => {
stdoutData += data.toString();
});
child.stderr?.on('data', (data: Buffer) => {
stderrData += data.toString();
});
return new Promise<void>((resolve, reject) => {
child.on('exit', code => {
const allOutput = stdoutData + '\n' + stderrData;
const lines = allOutput
.split(/\r?\n/)
.map(line => line.replace(ansiRegex, '').trim())
.map(line => line.replace(timestampRegex, ''))
.filter(line => line.length > 0)
.filter(line => !/Starting compilation|File change detected|Compilation complete/i.test(line));
runReporter(lines.join('\n'));
if (code === 0) {
Promise.resolve(onComplete?.()).then(() => resolve(), reject);
} else {
+27 -16
View File
@@ -15,6 +15,7 @@ import { getVersion } from '../lib/getVersion.ts';
import product from '../../product.json' with { type: 'json' };
import packageJson from '../../package.json' with { type: 'json' };
import { useEsbuildTranspile } from '../buildConfig.ts';
import { isWebExtension, type IScannedBuiltinExtension } from '../lib/extensions.ts';
const globAsync = promisify(glob);
@@ -378,33 +379,43 @@ async function cleanDir(dir: string): Promise<void> {
* Scan for built-in extensions in the given directory.
* Returns an array of extension entries for the builtinExtensionsScannerService.
*/
function scanBuiltinExtensions(extensionsRoot: string): Array<{ extensionPath: string; packageJSON: unknown }> {
const result: Array<{ extensionPath: string; packageJSON: unknown }> = [];
function scanBuiltinExtensions(extensionsRoot: string): Array<IScannedBuiltinExtension> {
const scannedExtensions: Array<IScannedBuiltinExtension> = [];
const extensionsPath = path.join(REPO_ROOT, extensionsRoot);
if (!fs.existsSync(extensionsPath)) {
return result;
return scannedExtensions;
}
for (const entry of fs.readdirSync(extensionsPath, { withFileTypes: true })) {
if (!entry.isDirectory()) {
for (const extensionFolder of fs.readdirSync(extensionsPath)) {
const packageJSONPath = path.join(extensionsPath, extensionFolder, 'package.json');
if (!fs.existsSync(packageJSONPath)) {
continue;
}
const packageJsonPath = path.join(extensionsPath, entry.name, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const packageJSON = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
result.push({
extensionPath: entry.name,
packageJSON
});
} catch (e) {
// Skip invalid extensions
try {
const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath, 'utf8'));
if (!isWebExtension(packageJSON)) {
continue;
}
const children = fs.readdirSync(path.join(extensionsPath, extensionFolder));
const packageNLSPath = children.filter(child => child === 'package.nls.json')[0];
const packageNLS = packageNLSPath ? JSON.parse(fs.readFileSync(path.join(extensionsPath, extensionFolder, packageNLSPath), 'utf8')) : undefined;
const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0];
scannedExtensions.push({
extensionPath: extensionFolder,
packageJSON,
packageNLS,
readmePath: readme ? path.join(extensionFolder, readme) : undefined,
changelogPath: changelog ? path.join(extensionFolder, changelog) : undefined,
});
} catch (e) {
// Skip invalid extensions
}
}
return result;
return scannedExtensions;
}
/**
+3 -3
View File
@@ -1069,9 +1069,9 @@
}
},
"node_modules/tar": {
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
"integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
+24 -21
View File
@@ -307,17 +307,17 @@
}
},
"node_modules/@azure/core-xml": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.4.4.tgz",
"integrity": "sha512-J4FYAqakGXcbfeZjwjMzjNcpcH4E+JtEBv+xcV1yL0Ydn/6wbQfeFKTCHh9wttAi0lmajHw7yBbHPRG+YHckZQ==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.5.0.tgz",
"integrity": "sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-xml-parser": "^4.4.1",
"tslib": "^2.6.2"
"fast-xml-parser": "^5.0.7",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=18.0.0"
"node": ">=20.0.0"
}
},
"node_modules/@azure/cosmos": {
@@ -5358,23 +5358,19 @@
"license": "BSD-3-Clause"
},
"node_modules/fast-xml-parser": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz",
"integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==",
"version": "5.3.6",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz",
"integrity": "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
},
{
"type": "paypal",
"url": "https://paypal.me/naturalintelligence"
}
],
"license": "MIT",
"dependencies": {
"strnum": "^1.0.5"
"strnum": "^2.1.2"
},
"bin": {
"fxparser": "src/cli/cli.js"
@@ -8933,10 +8929,16 @@
}
},
"node_modules/strnum": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz",
"integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/structured-source": {
@@ -9433,10 +9435,11 @@
}
},
"node_modules/tslib": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
"dev": true
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/tunnel": {
"version": "0.0.6",
+16 -16
View File
@@ -7,8 +7,8 @@
"foreground": "#bfbfbf",
"disabledForeground": "#666666",
"errorForeground": "#f48771",
"descriptionForeground": "#888888",
"icon.foreground": "#888888",
"descriptionForeground": "#8C8C8C",
"icon.foreground": "#8C8C8C",
"focusBorder": "#3994BCB3",
"textBlockQuote.background": "#242526",
"textBlockQuote.border": "#2A2B2CFF",
@@ -16,7 +16,7 @@
"textLink.foreground": "#48A0C7",
"textLink.activeForeground": "#53A5CA",
"textPreformat.background": "#262626",
"textPreformat.foreground": "#888888",
"textPreformat.foreground": "#8C8C8C",
"textSeparator.foreground": "#2a2a2aFF",
"button.background": "#3994BCF2",
"button.foreground": "#FFFFFF",
@@ -69,7 +69,7 @@
"list.warningForeground": "#e5ba7d",
"activityBar.background": "#191A1B",
"activityBar.foreground": "#bfbfbf",
"activityBar.inactiveForeground": "#888888",
"activityBar.inactiveForeground": "#8C8C8C",
"activityBar.border": "#2A2B2CFF",
"activityBar.activeBorder": "#bfbfbf",
"activityBar.activeFocusBorder": "#3994BCB3",
@@ -86,7 +86,7 @@
"titleBar.activeBackground": "#191A1B",
"titleBar.activeForeground": "#bfbfbf",
"titleBar.inactiveBackground": "#191A1B",
"titleBar.inactiveForeground": "#888888",
"titleBar.inactiveForeground": "#8C8C8C",
"titleBar.border": "#2A2B2CFF",
"menubar.selectionBackground": "#242526",
"menubar.selectionForeground": "#bfbfbf",
@@ -120,11 +120,11 @@
"editor.lineHighlightBackground": "#242526",
"editor.rangeHighlightBackground": "#242526",
"editorLink.activeForeground": "#3a94bc",
"editorWhitespace.foreground": "#8888884D",
"editorWhitespace.foreground": "#8C8C8C4D",
"editorIndentGuide.background": "#8384854D",
"editorIndentGuide.activeBackground": "#838485",
"editorRuler.foreground": "#848484",
"editorCodeLens.foreground": "#888888",
"editorCodeLens.foreground": "#8C8C8C",
"editorBracketMatch.background": "#3994BC55",
"editorBracketMatch.border": "#2A2B2CFF",
"editorWidget.background": "#202122",
@@ -143,12 +143,12 @@
"peekViewEditor.matchHighlightBackground": "#3994BC33",
"peekViewResult.background": "#191A1B",
"peekViewResult.fileForeground": "#bfbfbf",
"peekViewResult.lineForeground": "#888888",
"peekViewResult.lineForeground": "#8C8C8C",
"peekViewResult.matchHighlightBackground": "#3994BC33",
"peekViewResult.selectionBackground": "#3994BC26",
"peekViewResult.selectionForeground": "#bfbfbf",
"peekViewTitle.background": "#242526",
"peekViewTitleDescription.foreground": "#888888",
"peekViewTitleDescription.foreground": "#8C8C8C",
"peekViewTitleLabel.foreground": "#bfbfbf",
"editorGutter.background": "#121314",
"editorGutter.addedBackground": "#72C892",
@@ -166,15 +166,15 @@
"panel.border": "#2A2B2CFF",
"panelTitle.activeBorder": "#3994BC",
"panelTitle.activeForeground": "#bfbfbf",
"panelTitle.inactiveForeground": "#888888",
"panelTitle.inactiveForeground": "#8C8C8C",
"statusBar.background": "#191A1B",
"statusBar.foreground": "#888888",
"statusBar.foreground": "#8C8C8C",
"statusBar.border": "#2A2B2CFF",
"statusBar.focusBorder": "#3994BCB3",
"statusBar.debuggingBackground": "#3994BC",
"statusBar.debuggingForeground": "#FFFFFF",
"statusBar.noFolderBackground": "#191A1B",
"statusBar.noFolderForeground": "#888888",
"statusBar.noFolderForeground": "#8C8C8C",
"statusBarItem.activeBackground": "#4B4C4D",
"statusBarItem.hoverBackground": "#262728",
"statusBarItem.focusBorder": "#3994BCB3",
@@ -184,7 +184,7 @@
"tab.activeBackground": "#121314",
"tab.activeForeground": "#bfbfbf",
"tab.inactiveBackground": "#191A1B",
"tab.inactiveForeground": "#888888",
"tab.inactiveForeground": "#8C8C8C",
"tab.border": "#2A2B2CFF",
"tab.lastPinnedBorder": "#2A2B2CFF",
"tab.activeBorder": "#121314",
@@ -192,12 +192,12 @@
"tab.hoverBackground": "#262728",
"tab.hoverForeground": "#bfbfbf",
"tab.unfocusedActiveBackground": "#121314",
"tab.unfocusedActiveForeground": "#888888",
"tab.unfocusedActiveForeground": "#8C8C8C",
"tab.unfocusedInactiveBackground": "#191A1B",
"tab.unfocusedInactiveForeground": "#444444",
"editorGroupHeader.tabsBackground": "#191A1B",
"editorGroupHeader.tabsBorder": "#2A2B2CFF",
"breadcrumb.foreground": "#888888",
"breadcrumb.foreground": "#8C8C8C",
"breadcrumb.background": "#121314",
"breadcrumb.focusForeground": "#bfbfbf",
"breadcrumb.activeSelectionForeground": "#bfbfbf",
@@ -612,7 +612,7 @@
"markup.fenced_code"
],
"settings": {
"foreground": "#888888"
"foreground": "#8C8C8C"
}
},
{
+36 -34
View File
@@ -7,24 +7,25 @@
"foreground": "#202020",
"disabledForeground": "#BBBBBB",
"errorForeground": "#ad0707",
"descriptionForeground": "#555555",
"icon.foreground": "#666666",
"descriptionForeground": "#606060",
"icon.foreground": "#606060",
"focusBorder": "#0069CCFF",
"textBlockQuote.background": "#EDEDED",
"textBlockQuote.background": "#EAEAEA",
"textBlockQuote.border": "#F2F3F4FF",
"textCodeBlock.background": "#EDEDED",
"textCodeBlock.background": "#EAEAEA",
"textLink.foreground": "#0069CC",
"textLink.activeForeground": "#0069CC",
"textPreformat.foreground": "#666666",
"textPreformat.foreground": "#606060",
"textPreformat.background": "#ECECEC",
"textSeparator.foreground": "#EEEEEEFF",
"button.background": "#0069CC",
"button.foreground": "#FFFFFF",
"button.hoverBackground": "#0063C1",
"button.border": "#EEEEF1",
"button.secondaryBackground": "#EDEDED",
"button.secondaryBackground": "#EAEAEA",
"button.secondaryForeground": "#202020",
"button.secondaryHoverBackground": "#EEEEEE",
"checkbox.background": "#EDEDED",
"button.secondaryHoverBackground": "#F2F3F4",
"checkbox.background": "#EAEAEA",
"checkbox.border": "#D8D8D8",
"checkbox.foreground": "#202020",
"dropdown.background": "#FFFFFF",
@@ -61,11 +62,11 @@
"badge.background": "#0069CC",
"badge.foreground": "#FFFFFF",
"progressBar.background": "#0069CC",
"list.activeSelectionBackground": "#0069CC44",
"list.activeSelectionBackground": "#0069CC1A",
"list.activeSelectionForeground": "#202020",
"list.inactiveSelectionBackground": "#E0E0E0",
"list.inactiveSelectionBackground": "#DADADA99",
"list.inactiveSelectionForeground": "#202020",
"list.hoverBackground": "#EEEEEE",
"list.hoverBackground": "#DADADA4f",
"list.hoverForeground": "#202020",
"list.dropBackground": "#0069CC15",
"list.focusBackground": "#0069CC1A",
@@ -77,7 +78,7 @@
"list.warningForeground": "#667309",
"activityBar.background": "#FAFAFD",
"activityBar.foreground": "#202020",
"activityBar.inactiveForeground": "#666666",
"activityBar.inactiveForeground": "#606060",
"activityBar.border": "#F2F3F4FF",
"activityBar.activeBorder": "#000000",
"activityBar.activeFocusBorder": "#0069CCFF",
@@ -94,9 +95,9 @@
"titleBar.activeBackground": "#FAFAFD",
"titleBar.activeForeground": "#424242",
"titleBar.inactiveBackground": "#FAFAFD",
"titleBar.inactiveForeground": "#666666",
"titleBar.inactiveForeground": "#606060",
"titleBar.border": "#F2F3F4FF",
"menubar.selectionBackground": "#EDEDED",
"menubar.selectionBackground": "#EAEAEA",
"menubar.selectionForeground": "#202020",
"menu.background": "#FAFAFD",
"menu.foreground": "#202020",
@@ -111,7 +112,7 @@
"commandCenter.border": "#D8D8D8",
"editor.background": "#FFFFFF",
"editor.foreground": "#202020",
"editorLineNumber.foreground": "#666666",
"editorLineNumber.foreground": "#606060",
"editorLineNumber.activeForeground": "#202020",
"editorCursor.foreground": "#202020",
"editor.selectionBackground": "#0069CC1A",
@@ -121,16 +122,16 @@
"editor.wordHighlightStrongBackground": "#0069CC26",
"editor.findMatchBackground": "#0069CC40",
"editor.findMatchHighlightBackground": "#0069CC1A",
"editor.findRangeHighlightBackground": "#EDEDED",
"editor.hoverHighlightBackground": "#EDEDED",
"editor.lineHighlightBackground": "#EDEDED40",
"editor.rangeHighlightBackground": "#EDEDED",
"editor.findRangeHighlightBackground": "#EAEAEA",
"editor.hoverHighlightBackground": "#EAEAEA",
"editor.lineHighlightBackground": "#EAEAEA40",
"editor.rangeHighlightBackground": "#EAEAEA",
"editorLink.activeForeground": "#0069CC",
"editorWhitespace.foreground": "#66666640",
"editorWhitespace.foreground": "#60606040",
"editorIndentGuide.background": "#F7F7F740",
"editorIndentGuide.activeBackground": "#EEEEEE",
"editorRuler.foreground": "#F7F7F7",
"editorCodeLens.foreground": "#666666",
"editorCodeLens.foreground": "#606060",
"editorBracketMatch.background": "#0069CC40",
"editorBracketMatch.border": "#F2F3F4FF",
"editorWidget.background": "#F0F0F3",
@@ -148,12 +149,12 @@
"peekViewEditor.matchHighlightBackground": "#0069CC33",
"peekViewResult.background": "#F0F0F3",
"peekViewResult.fileForeground": "#202020",
"peekViewResult.lineForeground": "#666666",
"peekViewResult.lineForeground": "#606060",
"peekViewResult.matchHighlightBackground": "#0069CC33",
"peekViewResult.selectionBackground": "#0069CC26",
"peekViewResult.selectionForeground": "#202020",
"peekViewTitle.background": "#F0F0F3",
"peekViewTitleDescription.foreground": "#666666",
"peekViewTitleDescription.foreground": "#606060",
"peekViewTitleLabel.foreground": "#202020",
"editorGutter.addedBackground": "#587c0c",
"editorGutter.deletedBackground": "#ad0707",
@@ -171,17 +172,17 @@
"panel.border": "#F2F3F4FF",
"panelTitle.activeBorder": "#000000",
"panelTitle.activeForeground": "#202020",
"panelTitle.inactiveForeground": "#666666",
"panelTitle.inactiveForeground": "#606060",
"statusBar.background": "#FAFAFD",
"statusBar.foreground": "#666666",
"statusBar.foreground": "#606060",
"statusBar.border": "#F2F3F4FF",
"statusBar.focusBorder": "#0069CCFF",
"statusBar.debuggingBackground": "#0069CC",
"statusBar.debuggingForeground": "#FFFFFF",
"statusBar.noFolderBackground": "#F0F0F3",
"statusBar.noFolderForeground": "#666666",
"statusBar.noFolderForeground": "#606060",
"statusBarItem.activeBackground": "#EEEEEE",
"statusBarItem.hoverBackground": "#EEEEEE",
"statusBarItem.hoverBackground": "#DADADA4f",
"statusBarItem.focusBorder": "#0069CCFF",
"statusBarItem.prominentBackground": "#0069CCDD",
"statusBarItem.prominentForeground": "#FFFFFF",
@@ -190,30 +191,30 @@
"tab.activeBackground": "#FFFFFF",
"tab.activeForeground": "#202020",
"tab.inactiveBackground": "#FAFAFD",
"tab.inactiveForeground": "#666666",
"tab.inactiveForeground": "#606060",
"tab.border": "#F2F3F4FF",
"tab.lastPinnedBorder": "#F2F3F4FF",
"tab.activeBorder": "#FAFAFD",
"tab.activeBorderTop": "#000000",
"tab.hoverBackground": "#EEEEEE",
"tab.hoverBackground": "#DADADA4f",
"tab.hoverForeground": "#202020",
"tab.unfocusedActiveBackground": "#FAFAFD",
"tab.unfocusedActiveForeground": "#666666",
"tab.unfocusedActiveForeground": "#606060",
"tab.unfocusedInactiveBackground": "#FAFAFD",
"tab.unfocusedInactiveForeground": "#BBBBBB",
"editorGroupHeader.tabsBackground": "#FAFAFD",
"editorGroupHeader.tabsBorder": "#F2F3F4FF",
"breadcrumb.foreground": "#666666",
"breadcrumb.foreground": "#606060",
"breadcrumb.background": "#FFFFFF",
"breadcrumb.focusForeground": "#202020",
"breadcrumb.activeSelectionForeground": "#202020",
"breadcrumbPicker.background": "#F0F0F3",
"notificationCenter.border": "#F2F3F4FF",
"notificationCenterHeader.foreground": "#202020",
"notificationCenterHeader.background": "#F0F0F3",
"notificationCenterHeader.background": "#FAFAFD",
"notificationToast.border": "#F2F3F4FF",
"notifications.foreground": "#202020",
"notifications.background": "#F0F0F3",
"notifications.background": "#FAFAFD",
"notifications.border": "#F2F3F4FF",
"notificationLink.foreground": "#0069CC",
"notificationsWarningIcon.foreground": "#B69500",
@@ -258,6 +259,7 @@
"quickInputTitle.background": "#F0F0F3",
"chat.requestBubbleBackground": "#EEF4FB",
"chat.requestBubbleHoverBackground": "#E6EDFA",
"chat.thinkingShimmer": "#999999",
"editorCommentsWidget.rangeBackground": "#EEF4FB",
"editorCommentsWidget.rangeActiveBackground": "#E6EDFA",
"charts.foreground": "#202020",
@@ -617,7 +619,7 @@
"markup.fenced_code"
],
"settings": {
"foreground": "#666666"
"foreground": "#606060"
}
},
{
+24 -6
View File
@@ -61,11 +61,16 @@
}
/* Ensure iframe containers in pane-body render above sidebar z-index */
.monaco-workbench > div[data-keybinding-context],
.monaco-workbench > div[data-keybinding-context] {
z-index: 50 !important;
}
/* Ensure in-editor pane iframes render below sidebar z-index */
.monaco-workbench > div[data-parent-flow-to-element-id] {
z-index: 0 !important;
}
/* Ensure webview containers render above sidebar z-index */
.monaco-workbench .part.sidebar .webview,
.monaco-workbench .part.sidebar .webview-container,
@@ -200,7 +205,7 @@
background-color: color-mix(in srgb, var(--vscode-list-hoverBackground) 95%, black) !important;
}
.quick-input-list .quick-input-list-entry .quick-input-list-separator {
.monaco-workbench .quick-input-list .quick-input-list-entry .quick-input-list-separator {
height: 16px;
margin-top: 2px;
display: flex;
@@ -208,14 +213,18 @@
font-size: 11px;
padding: 0 4px 1px 4px;
border-radius: var(--vscode-cornerRadius-small) !important;
background: color-mix(in srgb, var(--vscode-badge-background) 50%, transparent) !important;
background: color-mix(in srgb, var(--vscode-badge-background) 70%, transparent) !important;
color: var(--vscode-badge-foreground) !important;
margin-right: 8px;
}
.monaco-list-row.focused .quick-input-list-entry .quick-input-list-separator,
.monaco-list-row.selected .quick-input-list-entry .quick-input-list-separator,
.monaco-list-row:hover .quick-input-list-entry .quick-input-list-separator {
.monaco-workbench.vs-dark .quick-input-list .quick-input-list-entry .quick-input-list-separator {
background: color-mix(in srgb, var(--vscode-badge-background) 50%, transparent) !important;
}
.monaco-workbench .monaco-list-row.focused .quick-input-list-entry .quick-input-list-separator,
.monaco-workbench .monaco-list-row.selected .quick-input-list-entry .quick-input-list-separator,
.monaco-workbench .monaco-list-row:hover .quick-input-list-entry .quick-input-list-separator {
background: transparent !important;
color: inherit !important;
padding: 0;
@@ -437,6 +446,15 @@
box-shadow: var(--shadow-sm);
}
.monaco-workbench .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget {
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--vscode-badge-background) 70%, transparent) !important;
}
.monaco-workbench.vs-dark .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget {
background: color-mix(in srgb, var(--vscode-badge-background) 50%, transparent) !important;
}
/* Welcome Tiles */
.monaco-workbench .part.editor .welcomePageContainer .tile {
box-shadow: var(--shadow-md);
@@ -156,7 +156,8 @@
{
"scope": "markup.italic",
"settings": {
"fontStyle": "italic"
"fontStyle": "italic",
"foreground": "#C586C0"
}
},
{
@@ -122,7 +122,8 @@
{
"scope": "markup.italic",
"settings": {
"fontStyle": "italic"
"fontStyle": "italic",
"foreground": "#800080"
}
},
{
@@ -159,7 +159,8 @@
{
"scope": "markup.italic",
"settings": {
"fontStyle": "italic"
"fontStyle": "italic",
"foreground": "#800080"
}
},
{
@@ -1809,42 +1809,42 @@
"c": "_",
"t": "text.html.markdown meta.paragraph.markdown markup.italic.markdown punctuation.definition.italic.markdown",
"r": {
"dark_plus": "default: #D4D4D4",
"light_plus": "default: #000000",
"dark_vs": "default: #D4D4D4",
"light_vs": "default: #000000",
"dark_plus": "markup.italic: #C586C0",
"light_plus": "markup.italic: #800080",
"dark_vs": "markup.italic: #C586C0",
"light_vs": "markup.italic: #800080",
"hc_black": "default: #FFFFFF",
"dark_modern": "default: #CCCCCC",
"hc_light": "default: #292929",
"light_modern": "default: #3B3B3B"
"dark_modern": "markup.italic: #C586C0",
"hc_light": "markup.italic: #800080",
"light_modern": "markup.italic: #800080"
}
},
{
"c": "italics",
"t": "text.html.markdown meta.paragraph.markdown markup.italic.markdown",
"r": {
"dark_plus": "default: #D4D4D4",
"light_plus": "default: #000000",
"dark_vs": "default: #D4D4D4",
"light_vs": "default: #000000",
"dark_plus": "markup.italic: #C586C0",
"light_plus": "markup.italic: #800080",
"dark_vs": "markup.italic: #C586C0",
"light_vs": "markup.italic: #800080",
"hc_black": "default: #FFFFFF",
"dark_modern": "default: #CCCCCC",
"hc_light": "default: #292929",
"light_modern": "default: #3B3B3B"
"dark_modern": "markup.italic: #C586C0",
"hc_light": "markup.italic: #800080",
"light_modern": "markup.italic: #800080"
}
},
{
"c": "_",
"t": "text.html.markdown meta.paragraph.markdown markup.italic.markdown punctuation.definition.italic.markdown",
"r": {
"dark_plus": "default: #D4D4D4",
"light_plus": "default: #000000",
"dark_vs": "default: #D4D4D4",
"light_vs": "default: #000000",
"dark_plus": "markup.italic: #C586C0",
"light_plus": "markup.italic: #800080",
"dark_vs": "markup.italic: #C586C0",
"light_vs": "markup.italic: #800080",
"hc_black": "default: #FFFFFF",
"dark_modern": "default: #CCCCCC",
"hc_light": "default: #292929",
"light_modern": "default: #3B3B3B"
"dark_modern": "markup.italic: #C586C0",
"hc_light": "markup.italic: #800080",
"light_modern": "markup.italic: #800080"
}
},
{
@@ -3,14 +3,14 @@
"c": "*italics*",
"t": "source.rst markup.italic",
"r": {
"dark_plus": "default: #D4D4D4",
"light_plus": "default: #000000",
"dark_vs": "default: #D4D4D4",
"light_vs": "default: #000000",
"dark_plus": "markup.italic: #C586C0",
"light_plus": "markup.italic: #800080",
"dark_vs": "markup.italic: #C586C0",
"light_vs": "markup.italic: #800080",
"hc_black": "default: #FFFFFF",
"dark_modern": "default: #CCCCCC",
"hc_light": "default: #292929",
"light_modern": "default: #3B3B3B"
"dark_modern": "markup.italic: #C586C0",
"hc_light": "markup.italic: #800080",
"light_modern": "markup.italic: #800080"
}
},
{
+52 -52
View File
@@ -30,16 +30,16 @@
"@vscode/windows-mutex": "^0.5.0",
"@vscode/windows-process-tree": "^0.6.0",
"@vscode/windows-registry": "^1.1.0",
"@xterm/addon-clipboard": "^0.3.0-beta.152",
"@xterm/addon-image": "^0.10.0-beta.152",
"@xterm/addon-ligatures": "^0.11.0-beta.152",
"@xterm/addon-progress": "^0.3.0-beta.152",
"@xterm/addon-search": "^0.17.0-beta.152",
"@xterm/addon-serialize": "^0.15.0-beta.152",
"@xterm/addon-unicode11": "^0.10.0-beta.152",
"@xterm/addon-webgl": "^0.20.0-beta.151",
"@xterm/headless": "^6.1.0-beta.152",
"@xterm/xterm": "^6.1.0-beta.152",
"@xterm/addon-clipboard": "^0.3.0-beta.165",
"@xterm/addon-image": "^0.10.0-beta.165",
"@xterm/addon-ligatures": "^0.11.0-beta.165",
"@xterm/addon-progress": "^0.3.0-beta.165",
"@xterm/addon-search": "^0.17.0-beta.165",
"@xterm/addon-serialize": "^0.15.0-beta.165",
"@xterm/addon-unicode11": "^0.10.0-beta.165",
"@xterm/addon-webgl": "^0.20.0-beta.164",
"@xterm/headless": "^6.1.0-beta.165",
"@xterm/xterm": "^6.1.0-beta.165",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"jschardet": "3.1.4",
@@ -150,7 +150,7 @@
"source-map": "0.6.1",
"source-map-support": "^0.3.2",
"style-loader": "^3.3.2",
"tar": "^7.5.7",
"tar": "^7.5.9",
"ts-loader": "^9.5.1",
"tsec": "0.2.7",
"tslib": "^2.6.3",
@@ -3909,30 +3909,30 @@
}
},
"node_modules/@xterm/addon-clipboard": {
"version": "0.3.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.152.tgz",
"integrity": "sha512-D+wFHTTNj1qzlSL1h15tgFh6JgK/SSaotkohtaKykkKFmkdGrtJq8PpINaFipRDrZXX0d9eOD+wrMfz6IG+5Yw==",
"version": "0.3.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.165.tgz",
"integrity": "sha512-48GUTZg7sKB7tQvtC7FcH22GxxO0cIUVM4hw068Oi3cJnxDLLPQDicPv70fFG7zysGxxEKE7A39GMtHhwFI75Q==",
"license": "MIT",
"dependencies": {
"js-base64": "^3.7.5"
},
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-image": {
"version": "0.10.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.152.tgz",
"integrity": "sha512-pyQ/hQr3O0gY1La+6ZXdh0tI/+6MmNo2eFPNyWzB21J02xMu6nc30+B/H9VlPSR3AXHno5U67AWra5Y4FrE+5A==",
"version": "0.10.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.165.tgz",
"integrity": "sha512-DwYvKRgytc1OYoJVwA/doOTT92K8asgvnt3FzsHt5D+XgniwdvM5nwjxv95p6UXv0kEOxQWFy3sNJl/4g/5pew==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-ligatures": {
"version": "0.11.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.152.tgz",
"integrity": "sha512-DglTaxmWHolTfryequU/7+Q4bjpDywt7UDsE3SdbC7O/9fa1qaOZMVlxKtRBtMBBzX5PXa+Ha4qAaMS2psr3UQ==",
"version": "0.11.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.165.tgz",
"integrity": "sha512-3nuPBH4ZrGYF+yj/tBB/+YaLRnn8qqbR9J9OcvM6aeDfboEeaFAYIpmdqjh+2Rl2JFTIgZoiS3dKLWaUUpk0Tw==",
"license": "MIT",
"dependencies": {
"lru-cache": "^6.0.0",
@@ -3942,7 +3942,7 @@
"node": ">8.0.0"
},
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-ligatures/node_modules/lru-cache": {
@@ -3964,63 +3964,63 @@
"license": "ISC"
},
"node_modules/@xterm/addon-progress": {
"version": "0.3.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.152.tgz",
"integrity": "sha512-H3qNwUaTNDRm51s8IzcYRinnQBSf7QDXWkcyAuDlprDJlR5BFhmGr9hpMV/KlCo2s6nhWrFjiwkd642DJ7McMg==",
"version": "0.3.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.165.tgz",
"integrity": "sha512-Jl+dhHkFBUafrXCECI/EepcGV1GYuU1X/0oXkPYu/VYfbmkjQSAidmfBAEyS+4+AUK5Lkf6yLdb1N13tZVexyg==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-search": {
"version": "0.17.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.152.tgz",
"integrity": "sha512-0T/xDg0yh3PlS9HWOioIrNGP0OfUp4MtBJ7M2sfR+h23KKa4gl1ec7S1TsGU4gsvEMBKG1TB6jReX4vlKGYc4A==",
"version": "0.17.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.165.tgz",
"integrity": "sha512-3KjonTDJl/8M6jI5nTJITVT+Z528d/5CgqRmn6IV+sDgRfr3W84RZNDsaxXsLoc0GDsxQIB74/FmnNykUQ5Yew==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-serialize": {
"version": "0.15.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.152.tgz",
"integrity": "sha512-GnhwKg0dkpAI1gZmm9L69Xjseal5pKwXFaMUxzm+Viajcp/PdqK1pEBJX5RndToNF0Ti3xu4e6BFO7dqY/J9TA==",
"version": "0.15.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.165.tgz",
"integrity": "sha512-NjXE+of4NJagrtHlzePBuWQ8a9pBFhhmQuvOhPj9W3CSi+VanuMoM/oRaT1TbR3efHk2JdCsKVDJScEzY8kdjw==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-unicode11": {
"version": "0.10.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.152.tgz",
"integrity": "sha512-2HSpMjbckGAmU/56CTGuEbCZJZHxUlfNJ2uzR4akZyVVLEmavC4thHVSGT7Ei1zzpHZsAg0y4WMbcp4wzpPv3g==",
"version": "0.10.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.165.tgz",
"integrity": "sha512-a6myeixOXDYeuOj0GK+/LWXbXXWanFVMvQRUMgC7wmUNGgSZiyJ8NPWzhAq6Vib4jSQ02pd+ux4ZtWs5kyvFLg==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-webgl": {
"version": "0.20.0-beta.151",
"resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.151.tgz",
"integrity": "sha512-3ogsmZKPKc8n9Mjik4jTmNYT2Nbboe/zqcjDNG7RONO3w/tUyoKQshYCMBxxGMNLDwvh3BQ/D9/6JvdNWA1ShA==",
"version": "0.20.0-beta.164",
"resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.164.tgz",
"integrity": "sha512-wXTi281yTWY1iAmRh21N6AhcEMopTjIm4xsdDdNmS5LbhxNuhVKNNIGKm5Zhd/G9fpn/vrfC4yZ6KA0lI/ZAxg==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/headless": {
"version": "6.1.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.152.tgz",
"integrity": "sha512-Hkt+KPuifM8kqDKtbHq1uIhqdZMQazKTl9zaqjcWY3Vogx7+JVr6F+eN89KHnrvhUUOmhAM0JQAIRv1O+upfUw==",
"version": "6.1.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.165.tgz",
"integrity": "sha512-GjAqUhEiY7gb12+yIItptgMKUwHMa7o39HpezD7sfNjYLjmvWQcB02jqUdVMsvjjAKTe2YJMXp3RkApeXdMRVg==",
"license": "MIT",
"workspaces": [
"addons/*"
]
},
"node_modules/@xterm/xterm": {
"version": "6.1.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.152.tgz",
"integrity": "sha512-XHJ5ab19V6tmcHmBE7k9IYjXSwTxUd0c7oKLa5J+ZO0+aiXE8UKh9OEDw1oyl5ZQhw9gn71cGEo4TpB58KhfoQ==",
"version": "6.1.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.165.tgz",
"integrity": "sha512-OUszO4HSmGPEw3EhboyIcNLQKJQKCDsYHv9kYFcaiK3biuNjGP0VAPVUJOLbf3V9fa1GLUUq+t985blqvTApoA==",
"license": "MIT",
"workspaces": [
"addons/*"
@@ -16389,9 +16389,9 @@
}
},
"node_modules/tar": {
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
"integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
+13 -13
View File
@@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.110.0",
"distro": "16be71799bd7ef33ea9b0206fb548ce74a47daa4",
"distro": "85914d5a600261a53306190177be48aa8f0cdfb4",
"author": {
"name": "Microsoft Corporation"
},
@@ -95,16 +95,16 @@
"@vscode/windows-mutex": "^0.5.0",
"@vscode/windows-process-tree": "^0.6.0",
"@vscode/windows-registry": "^1.1.0",
"@xterm/addon-clipboard": "^0.3.0-beta.152",
"@xterm/addon-image": "^0.10.0-beta.152",
"@xterm/addon-ligatures": "^0.11.0-beta.152",
"@xterm/addon-progress": "^0.3.0-beta.152",
"@xterm/addon-search": "^0.17.0-beta.152",
"@xterm/addon-serialize": "^0.15.0-beta.152",
"@xterm/addon-unicode11": "^0.10.0-beta.152",
"@xterm/addon-webgl": "^0.20.0-beta.151",
"@xterm/headless": "^6.1.0-beta.152",
"@xterm/xterm": "^6.1.0-beta.152",
"@xterm/addon-clipboard": "^0.3.0-beta.165",
"@xterm/addon-image": "^0.10.0-beta.165",
"@xterm/addon-ligatures": "^0.11.0-beta.165",
"@xterm/addon-progress": "^0.3.0-beta.165",
"@xterm/addon-search": "^0.17.0-beta.165",
"@xterm/addon-serialize": "^0.15.0-beta.165",
"@xterm/addon-unicode11": "^0.10.0-beta.165",
"@xterm/addon-webgl": "^0.20.0-beta.164",
"@xterm/headless": "^6.1.0-beta.165",
"@xterm/xterm": "^6.1.0-beta.165",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"jschardet": "3.1.4",
@@ -215,7 +215,7 @@
"source-map": "0.6.1",
"source-map-support": "^0.3.2",
"style-loader": "^3.3.2",
"tar": "^7.5.7",
"tar": "^7.5.9",
"ts-loader": "^9.5.1",
"tsec": "0.2.7",
"tslib": "^2.6.3",
@@ -244,4 +244,4 @@
"optionalDependencies": {
"windows-foreground-love": "0.6.1"
}
}
}
+48 -48
View File
@@ -22,16 +22,16 @@
"@vscode/vscode-languagedetection": "1.0.21",
"@vscode/windows-process-tree": "^0.6.0",
"@vscode/windows-registry": "^1.1.0",
"@xterm/addon-clipboard": "^0.3.0-beta.152",
"@xterm/addon-image": "^0.10.0-beta.152",
"@xterm/addon-ligatures": "^0.11.0-beta.152",
"@xterm/addon-progress": "^0.3.0-beta.152",
"@xterm/addon-search": "^0.17.0-beta.152",
"@xterm/addon-serialize": "^0.15.0-beta.152",
"@xterm/addon-unicode11": "^0.10.0-beta.152",
"@xterm/addon-webgl": "^0.20.0-beta.151",
"@xterm/headless": "^6.1.0-beta.152",
"@xterm/xterm": "^6.1.0-beta.152",
"@xterm/addon-clipboard": "^0.3.0-beta.165",
"@xterm/addon-image": "^0.10.0-beta.165",
"@xterm/addon-ligatures": "^0.11.0-beta.165",
"@xterm/addon-progress": "^0.3.0-beta.165",
"@xterm/addon-search": "^0.17.0-beta.165",
"@xterm/addon-serialize": "^0.15.0-beta.165",
"@xterm/addon-unicode11": "^0.10.0-beta.165",
"@xterm/addon-webgl": "^0.20.0-beta.164",
"@xterm/headless": "^6.1.0-beta.165",
"@xterm/xterm": "^6.1.0-beta.165",
"cookie": "^0.7.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
@@ -577,30 +577,30 @@
"license": "MIT"
},
"node_modules/@xterm/addon-clipboard": {
"version": "0.3.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.152.tgz",
"integrity": "sha512-D+wFHTTNj1qzlSL1h15tgFh6JgK/SSaotkohtaKykkKFmkdGrtJq8PpINaFipRDrZXX0d9eOD+wrMfz6IG+5Yw==",
"version": "0.3.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.165.tgz",
"integrity": "sha512-48GUTZg7sKB7tQvtC7FcH22GxxO0cIUVM4hw068Oi3cJnxDLLPQDicPv70fFG7zysGxxEKE7A39GMtHhwFI75Q==",
"license": "MIT",
"dependencies": {
"js-base64": "^3.7.5"
},
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-image": {
"version": "0.10.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.152.tgz",
"integrity": "sha512-pyQ/hQr3O0gY1La+6ZXdh0tI/+6MmNo2eFPNyWzB21J02xMu6nc30+B/H9VlPSR3AXHno5U67AWra5Y4FrE+5A==",
"version": "0.10.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.165.tgz",
"integrity": "sha512-DwYvKRgytc1OYoJVwA/doOTT92K8asgvnt3FzsHt5D+XgniwdvM5nwjxv95p6UXv0kEOxQWFy3sNJl/4g/5pew==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-ligatures": {
"version": "0.11.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.152.tgz",
"integrity": "sha512-DglTaxmWHolTfryequU/7+Q4bjpDywt7UDsE3SdbC7O/9fa1qaOZMVlxKtRBtMBBzX5PXa+Ha4qAaMS2psr3UQ==",
"version": "0.11.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.165.tgz",
"integrity": "sha512-3nuPBH4ZrGYF+yj/tBB/+YaLRnn8qqbR9J9OcvM6aeDfboEeaFAYIpmdqjh+2Rl2JFTIgZoiS3dKLWaUUpk0Tw==",
"license": "MIT",
"dependencies": {
"lru-cache": "^6.0.0",
@@ -610,67 +610,67 @@
"node": ">8.0.0"
},
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-progress": {
"version": "0.3.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.152.tgz",
"integrity": "sha512-H3qNwUaTNDRm51s8IzcYRinnQBSf7QDXWkcyAuDlprDJlR5BFhmGr9hpMV/KlCo2s6nhWrFjiwkd642DJ7McMg==",
"version": "0.3.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.165.tgz",
"integrity": "sha512-Jl+dhHkFBUafrXCECI/EepcGV1GYuU1X/0oXkPYu/VYfbmkjQSAidmfBAEyS+4+AUK5Lkf6yLdb1N13tZVexyg==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-search": {
"version": "0.17.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.152.tgz",
"integrity": "sha512-0T/xDg0yh3PlS9HWOioIrNGP0OfUp4MtBJ7M2sfR+h23KKa4gl1ec7S1TsGU4gsvEMBKG1TB6jReX4vlKGYc4A==",
"version": "0.17.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.165.tgz",
"integrity": "sha512-3KjonTDJl/8M6jI5nTJITVT+Z528d/5CgqRmn6IV+sDgRfr3W84RZNDsaxXsLoc0GDsxQIB74/FmnNykUQ5Yew==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-serialize": {
"version": "0.15.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.152.tgz",
"integrity": "sha512-GnhwKg0dkpAI1gZmm9L69Xjseal5pKwXFaMUxzm+Viajcp/PdqK1pEBJX5RndToNF0Ti3xu4e6BFO7dqY/J9TA==",
"version": "0.15.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.165.tgz",
"integrity": "sha512-NjXE+of4NJagrtHlzePBuWQ8a9pBFhhmQuvOhPj9W3CSi+VanuMoM/oRaT1TbR3efHk2JdCsKVDJScEzY8kdjw==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-unicode11": {
"version": "0.10.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.152.tgz",
"integrity": "sha512-2HSpMjbckGAmU/56CTGuEbCZJZHxUlfNJ2uzR4akZyVVLEmavC4thHVSGT7Ei1zzpHZsAg0y4WMbcp4wzpPv3g==",
"version": "0.10.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.165.tgz",
"integrity": "sha512-a6myeixOXDYeuOj0GK+/LWXbXXWanFVMvQRUMgC7wmUNGgSZiyJ8NPWzhAq6Vib4jSQ02pd+ux4ZtWs5kyvFLg==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-webgl": {
"version": "0.20.0-beta.151",
"resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.151.tgz",
"integrity": "sha512-3ogsmZKPKc8n9Mjik4jTmNYT2Nbboe/zqcjDNG7RONO3w/tUyoKQshYCMBxxGMNLDwvh3BQ/D9/6JvdNWA1ShA==",
"version": "0.20.0-beta.164",
"resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.164.tgz",
"integrity": "sha512-wXTi281yTWY1iAmRh21N6AhcEMopTjIm4xsdDdNmS5LbhxNuhVKNNIGKm5Zhd/G9fpn/vrfC4yZ6KA0lI/ZAxg==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/headless": {
"version": "6.1.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.152.tgz",
"integrity": "sha512-Hkt+KPuifM8kqDKtbHq1uIhqdZMQazKTl9zaqjcWY3Vogx7+JVr6F+eN89KHnrvhUUOmhAM0JQAIRv1O+upfUw==",
"version": "6.1.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.165.tgz",
"integrity": "sha512-GjAqUhEiY7gb12+yIItptgMKUwHMa7o39HpezD7sfNjYLjmvWQcB02jqUdVMsvjjAKTe2YJMXp3RkApeXdMRVg==",
"license": "MIT",
"workspaces": [
"addons/*"
]
},
"node_modules/@xterm/xterm": {
"version": "6.1.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.152.tgz",
"integrity": "sha512-XHJ5ab19V6tmcHmBE7k9IYjXSwTxUd0c7oKLa5J+ZO0+aiXE8UKh9OEDw1oyl5ZQhw9gn71cGEo4TpB58KhfoQ==",
"version": "6.1.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.165.tgz",
"integrity": "sha512-OUszO4HSmGPEw3EhboyIcNLQKJQKCDsYHv9kYFcaiK3biuNjGP0VAPVUJOLbf3V9fa1GLUUq+t985blqvTApoA==",
"license": "MIT",
"workspaces": [
"addons/*"
+10 -10
View File
@@ -17,16 +17,16 @@
"@vscode/vscode-languagedetection": "1.0.21",
"@vscode/windows-process-tree": "^0.6.0",
"@vscode/windows-registry": "^1.1.0",
"@xterm/addon-clipboard": "^0.3.0-beta.152",
"@xterm/addon-image": "^0.10.0-beta.152",
"@xterm/addon-ligatures": "^0.11.0-beta.152",
"@xterm/addon-progress": "^0.3.0-beta.152",
"@xterm/addon-search": "^0.17.0-beta.152",
"@xterm/addon-serialize": "^0.15.0-beta.152",
"@xterm/addon-unicode11": "^0.10.0-beta.152",
"@xterm/addon-webgl": "^0.20.0-beta.151",
"@xterm/headless": "^6.1.0-beta.152",
"@xterm/xterm": "^6.1.0-beta.152",
"@xterm/addon-clipboard": "^0.3.0-beta.165",
"@xterm/addon-image": "^0.10.0-beta.165",
"@xterm/addon-ligatures": "^0.11.0-beta.165",
"@xterm/addon-progress": "^0.3.0-beta.165",
"@xterm/addon-search": "^0.17.0-beta.165",
"@xterm/addon-serialize": "^0.15.0-beta.165",
"@xterm/addon-unicode11": "^0.10.0-beta.165",
"@xterm/addon-webgl": "^0.20.0-beta.164",
"@xterm/headless": "^6.1.0-beta.165",
"@xterm/xterm": "^6.1.0-beta.165",
"cookie": "^0.7.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
+44 -44
View File
@@ -14,15 +14,15 @@
"@vscode/iconv-lite-umd": "0.7.1",
"@vscode/tree-sitter-wasm": "^0.3.0",
"@vscode/vscode-languagedetection": "1.0.21",
"@xterm/addon-clipboard": "^0.3.0-beta.152",
"@xterm/addon-image": "^0.10.0-beta.152",
"@xterm/addon-ligatures": "^0.11.0-beta.152",
"@xterm/addon-progress": "^0.3.0-beta.152",
"@xterm/addon-search": "^0.17.0-beta.152",
"@xterm/addon-serialize": "^0.15.0-beta.152",
"@xterm/addon-unicode11": "^0.10.0-beta.152",
"@xterm/addon-webgl": "^0.20.0-beta.151",
"@xterm/xterm": "^6.1.0-beta.152",
"@xterm/addon-clipboard": "^0.3.0-beta.165",
"@xterm/addon-image": "^0.10.0-beta.165",
"@xterm/addon-ligatures": "^0.11.0-beta.165",
"@xterm/addon-progress": "^0.3.0-beta.165",
"@xterm/addon-search": "^0.17.0-beta.165",
"@xterm/addon-serialize": "^0.15.0-beta.165",
"@xterm/addon-unicode11": "^0.10.0-beta.165",
"@xterm/addon-webgl": "^0.20.0-beta.164",
"@xterm/xterm": "^6.1.0-beta.165",
"jschardet": "3.1.4",
"katex": "^0.16.22",
"tas-client": "0.3.1",
@@ -99,30 +99,30 @@
}
},
"node_modules/@xterm/addon-clipboard": {
"version": "0.3.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.152.tgz",
"integrity": "sha512-D+wFHTTNj1qzlSL1h15tgFh6JgK/SSaotkohtaKykkKFmkdGrtJq8PpINaFipRDrZXX0d9eOD+wrMfz6IG+5Yw==",
"version": "0.3.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.165.tgz",
"integrity": "sha512-48GUTZg7sKB7tQvtC7FcH22GxxO0cIUVM4hw068Oi3cJnxDLLPQDicPv70fFG7zysGxxEKE7A39GMtHhwFI75Q==",
"license": "MIT",
"dependencies": {
"js-base64": "^3.7.5"
},
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-image": {
"version": "0.10.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.152.tgz",
"integrity": "sha512-pyQ/hQr3O0gY1La+6ZXdh0tI/+6MmNo2eFPNyWzB21J02xMu6nc30+B/H9VlPSR3AXHno5U67AWra5Y4FrE+5A==",
"version": "0.10.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.165.tgz",
"integrity": "sha512-DwYvKRgytc1OYoJVwA/doOTT92K8asgvnt3FzsHt5D+XgniwdvM5nwjxv95p6UXv0kEOxQWFy3sNJl/4g/5pew==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-ligatures": {
"version": "0.11.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.152.tgz",
"integrity": "sha512-DglTaxmWHolTfryequU/7+Q4bjpDywt7UDsE3SdbC7O/9fa1qaOZMVlxKtRBtMBBzX5PXa+Ha4qAaMS2psr3UQ==",
"version": "0.11.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.165.tgz",
"integrity": "sha512-3nuPBH4ZrGYF+yj/tBB/+YaLRnn8qqbR9J9OcvM6aeDfboEeaFAYIpmdqjh+2Rl2JFTIgZoiS3dKLWaUUpk0Tw==",
"license": "MIT",
"dependencies": {
"lru-cache": "^6.0.0",
@@ -132,58 +132,58 @@
"node": ">8.0.0"
},
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-progress": {
"version": "0.3.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.152.tgz",
"integrity": "sha512-H3qNwUaTNDRm51s8IzcYRinnQBSf7QDXWkcyAuDlprDJlR5BFhmGr9hpMV/KlCo2s6nhWrFjiwkd642DJ7McMg==",
"version": "0.3.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.165.tgz",
"integrity": "sha512-Jl+dhHkFBUafrXCECI/EepcGV1GYuU1X/0oXkPYu/VYfbmkjQSAidmfBAEyS+4+AUK5Lkf6yLdb1N13tZVexyg==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-search": {
"version": "0.17.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.152.tgz",
"integrity": "sha512-0T/xDg0yh3PlS9HWOioIrNGP0OfUp4MtBJ7M2sfR+h23KKa4gl1ec7S1TsGU4gsvEMBKG1TB6jReX4vlKGYc4A==",
"version": "0.17.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.165.tgz",
"integrity": "sha512-3KjonTDJl/8M6jI5nTJITVT+Z528d/5CgqRmn6IV+sDgRfr3W84RZNDsaxXsLoc0GDsxQIB74/FmnNykUQ5Yew==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-serialize": {
"version": "0.15.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.152.tgz",
"integrity": "sha512-GnhwKg0dkpAI1gZmm9L69Xjseal5pKwXFaMUxzm+Viajcp/PdqK1pEBJX5RndToNF0Ti3xu4e6BFO7dqY/J9TA==",
"version": "0.15.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.165.tgz",
"integrity": "sha512-NjXE+of4NJagrtHlzePBuWQ8a9pBFhhmQuvOhPj9W3CSi+VanuMoM/oRaT1TbR3efHk2JdCsKVDJScEzY8kdjw==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-unicode11": {
"version": "0.10.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.152.tgz",
"integrity": "sha512-2HSpMjbckGAmU/56CTGuEbCZJZHxUlfNJ2uzR4akZyVVLEmavC4thHVSGT7Ei1zzpHZsAg0y4WMbcp4wzpPv3g==",
"version": "0.10.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.165.tgz",
"integrity": "sha512-a6myeixOXDYeuOj0GK+/LWXbXXWanFVMvQRUMgC7wmUNGgSZiyJ8NPWzhAq6Vib4jSQ02pd+ux4ZtWs5kyvFLg==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/addon-webgl": {
"version": "0.20.0-beta.151",
"resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.151.tgz",
"integrity": "sha512-3ogsmZKPKc8n9Mjik4jTmNYT2Nbboe/zqcjDNG7RONO3w/tUyoKQshYCMBxxGMNLDwvh3BQ/D9/6JvdNWA1ShA==",
"version": "0.20.0-beta.164",
"resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.164.tgz",
"integrity": "sha512-wXTi281yTWY1iAmRh21N6AhcEMopTjIm4xsdDdNmS5LbhxNuhVKNNIGKm5Zhd/G9fpn/vrfC4yZ6KA0lI/ZAxg==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^6.1.0-beta.152"
"@xterm/xterm": "^6.1.0-beta.165"
}
},
"node_modules/@xterm/xterm": {
"version": "6.1.0-beta.152",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.152.tgz",
"integrity": "sha512-XHJ5ab19V6tmcHmBE7k9IYjXSwTxUd0c7oKLa5J+ZO0+aiXE8UKh9OEDw1oyl5ZQhw9gn71cGEo4TpB58KhfoQ==",
"version": "6.1.0-beta.165",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.165.tgz",
"integrity": "sha512-OUszO4HSmGPEw3EhboyIcNLQKJQKCDsYHv9kYFcaiK3biuNjGP0VAPVUJOLbf3V9fa1GLUUq+t985blqvTApoA==",
"license": "MIT",
"workspaces": [
"addons/*"
+9 -9
View File
@@ -9,15 +9,15 @@
"@vscode/iconv-lite-umd": "0.7.1",
"@vscode/tree-sitter-wasm": "^0.3.0",
"@vscode/vscode-languagedetection": "1.0.21",
"@xterm/addon-clipboard": "^0.3.0-beta.152",
"@xterm/addon-image": "^0.10.0-beta.152",
"@xterm/addon-ligatures": "^0.11.0-beta.152",
"@xterm/addon-progress": "^0.3.0-beta.152",
"@xterm/addon-search": "^0.17.0-beta.152",
"@xterm/addon-serialize": "^0.15.0-beta.152",
"@xterm/addon-unicode11": "^0.10.0-beta.152",
"@xterm/addon-webgl": "^0.20.0-beta.151",
"@xterm/xterm": "^6.1.0-beta.152",
"@xterm/addon-clipboard": "^0.3.0-beta.165",
"@xterm/addon-image": "^0.10.0-beta.165",
"@xterm/addon-ligatures": "^0.11.0-beta.165",
"@xterm/addon-progress": "^0.3.0-beta.165",
"@xterm/addon-search": "^0.17.0-beta.165",
"@xterm/addon-serialize": "^0.15.0-beta.165",
"@xterm/addon-unicode11": "^0.10.0-beta.165",
"@xterm/addon-webgl": "^0.20.0-beta.164",
"@xterm/xterm": "^6.1.0-beta.165",
"jschardet": "3.1.4",
"katex": "^0.16.22",
"tas-client": "0.3.1",
+1494 -792
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8
View File
@@ -37,7 +37,9 @@ import { IEncryptionMainService } from '../../platform/encryption/common/encrypt
import { EncryptionMainService } from '../../platform/encryption/electron-main/encryptionMainService.js';
import { NativeBrowserElementsMainService, INativeBrowserElementsMainService } from '../../platform/browserElements/electron-main/nativeBrowserElementsMainService.js';
import { ipcBrowserViewChannelName } from '../../platform/browserView/common/browserView.js';
import { ipcBrowserViewGroupChannelName } from '../../platform/browserView/common/browserViewGroup.js';
import { BrowserViewMainService, IBrowserViewMainService } from '../../platform/browserView/electron-main/browserViewMainService.js';
import { BrowserViewGroupMainService, IBrowserViewGroupMainService } from '../../platform/browserView/electron-main/browserViewGroupMainService.js';
import { BrowserViewCDPProxyServer, IBrowserViewCDPProxyServer } from '../../platform/browserView/electron-main/browserViewCDPProxyServer.js';
import { NativeParsedArgs } from '../../platform/environment/common/argv.js';
import { IEnvironmentMainService } from '../../platform/environment/electron-main/environmentMainService.js';
@@ -1043,6 +1045,7 @@ export class CodeApplication extends Disposable {
// Browser View
services.set(IBrowserViewCDPProxyServer, new SyncDescriptor(BrowserViewCDPProxyServer, undefined, true));
services.set(IBrowserViewMainService, new SyncDescriptor(BrowserViewMainService, undefined, false /* proxied to other processes */));
services.set(IBrowserViewGroupMainService, new SyncDescriptor(BrowserViewGroupMainService, undefined, false /* proxied to other processes */));
// Keyboard Layout
services.set(IKeyboardLayoutMainService, new SyncDescriptor(KeyboardLayoutMainService));
@@ -1206,6 +1209,11 @@ export class CodeApplication extends Disposable {
mainProcessElectronServer.registerChannel(ipcBrowserViewChannelName, browserViewChannel);
sharedProcessClient.then(client => client.registerChannel(ipcBrowserViewChannelName, browserViewChannel));
// Browser View Group
const browserViewGroupChannel = ProxyChannel.fromService(accessor.get(IBrowserViewGroupMainService), disposables);
mainProcessElectronServer.registerChannel(ipcBrowserViewGroupChannelName, browserViewGroupChannel);
sharedProcessClient.then(client => client.registerChannel(ipcBrowserViewGroupChannelName, browserViewGroupChannel));
// Signing
const signChannel = ProxyChannel.fromService(accessor.get(ISignService), disposables);
mainProcessElectronServer.registerChannel('sign', signChannel);
@@ -136,6 +136,7 @@ import { IMeteredConnectionService } from '../../../platform/meteredConnection/c
import { MeteredConnectionChannelClient, METERED_CONNECTION_CHANNEL } from '../../../platform/meteredConnection/common/meteredConnectionIpc.js';
import { IPlaywrightService } from '../../../platform/browserView/common/playwrightService.js';
import { PlaywrightService } from '../../../platform/browserView/node/playwrightService.js';
import { IBrowserViewGroupRemoteService, BrowserViewGroupRemoteService } from '../../../platform/browserView/node/browserViewGroupRemoteService.js';
class SharedProcessMain extends Disposable implements IClientConnectionFilter {
@@ -404,6 +405,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
services.set(ISharedWebContentExtractorService, new SyncDescriptor(SharedWebContentExtractorService));
// Playwright
services.set(IBrowserViewGroupRemoteService, new SyncDescriptor(BrowserViewGroupRemoteService));
services.set(IPlaywrightService, new SyncDescriptor(PlaywrightService));
return new InstantiationService(services);
@@ -513,6 +513,8 @@ export class InlineEditItem extends InlineSuggestionItemBase {
let inlineEditModelVersion = this._inlineEditModelVersion;
let newAction: InlineSuggestionAction | undefined;
const updatedTarget = TextModelValueReference.snapshot(textModel);
if (this.action?.kind === 'edit') { // TODO What about rename?
edits = edits.map(innerEdit => innerEdit.applyTextModelChanges(textModelChanges));
@@ -546,7 +548,7 @@ export class InlineEditItem extends InlineSuggestionItemBase {
snippetInfo: this.snippetInfo,
stringEdit: newEdit,
alternativeAction: this.action.alternativeAction,
target: this.originalTextRef,
target: updatedTarget,
};
} else if (this.action?.kind === 'jumpTo') {
const jumpToOffset = this.action.offset;
@@ -560,7 +562,7 @@ export class InlineEditItem extends InlineSuggestionItemBase {
kind: 'jumpTo',
position: newJumpToPosition,
offset: newJumpToOffset,
target: this.originalTextRef,
target: updatedTarget,
};
} else {
newAction = undefined;
@@ -582,7 +584,7 @@ export class InlineEditItem extends InlineSuggestionItemBase {
newDisplayLocation,
lastChangePartOfInlineEdit,
inlineEditModelVersion,
this.originalTextRef,
updatedTarget,
);
}
@@ -36,7 +36,7 @@ export class GutterIndicatorMenuContent {
constructor(
private readonly _editorObs: ObservableCodeEditor,
private readonly _data: InlineSuggestionGutterMenuData,
private readonly _close: (focusEditor: boolean) => void,
private readonly _close: (focusEditor: boolean, commandId?: string) => void,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@ICommandService private readonly _commandService: ICommandService,
@@ -59,12 +59,36 @@ export class GutterIndicatorMenuContent {
isActive: activeElement.map(v => v === options.id),
onHoverChange: v => activeElement.set(v ? options.id : undefined, undefined),
onAction: () => {
this._close(true);
return this._commandService.executeCommand(typeof options.commandId === 'string' ? options.commandId : options.commandId.get(), ...(options.commandArgs ?? []));
const commandId = typeof options.commandId === 'string' ? options.commandId : options.commandId.get();
this._close(true, commandId);
return this._commandService.executeCommand(commandId, ...(options.commandArgs ?? []));
},
};
};
const extensionCommandGroups = this._data.extensionCommands.map(group =>
group.map((c, idx) => option(createOptionArgs({
id: c.command.id + '_' + idx,
title: c.command.title,
icon: c.icon ?? Codicon.symbolEvent,
commandId: c.command.id,
commandArgs: c.command.arguments
})))
);
const extensionCommandNodes: ChildNode = [];
for (const group of extensionCommandGroups) {
if (group.length > 0) {
extensionCommandNodes.push(separator());
extensionCommandNodes.push(...group);
}
}
if (this._data.extensionCommandsOnly) {
// drop leading separator
return hoverContent(extensionCommandNodes.slice(1));
}
const title = header(this._data.displayName);
const gotoAndAccept = option(createOptionArgs({
@@ -88,14 +112,6 @@ export class GutterIndicatorMenuContent {
commandId: inlineSuggestCommitAlternativeActionId,
})) : undefined;
const extensionCommands = this._data.extensionCommands.map((c, idx) => option(createOptionArgs({
id: c.command.id + '_' + idx,
title: c.command.title,
icon: c.icon ?? Codicon.symbolEvent,
commandId: c.command.id,
commandArgs: c.command.arguments
})));
const showModelEnabled = false;
const modelOptions = showModelEnabled ? this._data.modelInfo?.models.map((m: { id: string; name: string }) => option({
title: m.name,
@@ -160,11 +176,10 @@ export class GutterIndicatorMenuContent {
toggleCollapsedMode,
modelOptions.length ? separator() : undefined,
...modelOptions,
extensionCommands.length ? separator() : undefined,
snooze,
settings,
...extensionCommands,
...extensionCommandNodes,
actionBarFooter ? separator() : undefined,
actionBarFooter
@@ -36,6 +36,7 @@ import { InlineSuggestAlternativeAction } from '../../../model/InlineSuggestAlte
import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js';
import { ThemeIcon } from '../../../../../../../base/common/themables.js';
import { IUserInteractionService } from '../../../../../../../platform/userInteraction/browser/userInteractionService.js';
import { Event, Emitter } from '../../../../../../../base/common/event.js';
/**
* Customization options for the gutter indicator appearance and behavior.
@@ -58,10 +59,11 @@ export class InlineEditsGutterIndicatorData {
export class InlineSuggestionGutterMenuData {
public static fromInlineSuggestion(suggestion: InlineSuggestionItem): InlineSuggestionGutterMenuData {
const alternativeAction = suggestion.action?.kind === 'edit' ? suggestion.action.alternativeAction : undefined;
const commands = suggestion.source.inlineSuggestions.commands ?? [];
return new InlineSuggestionGutterMenuData(
suggestion.gutterMenuLinkAction,
suggestion.source.provider.displayName ?? localize('inlineSuggestion', "Inline Suggestion"),
suggestion.source.inlineSuggestions.commands ?? [],
commands.length > 0 ? [commands] : [],
alternativeAction,
suggestion.source.provider.modelInfo,
suggestion.source.provider.setModelId?.bind(suggestion.source.provider),
@@ -71,10 +73,11 @@ export class InlineSuggestionGutterMenuData {
constructor(
readonly action: Command | undefined,
readonly displayName: string,
readonly extensionCommands: InlineCompletionCommand[],
readonly extensionCommands: InlineCompletionCommand[][],
readonly alternativeAction: InlineSuggestAlternativeAction | undefined,
readonly modelInfo: IInlineCompletionModelInfo | undefined,
readonly setModelId: ((modelId: string) => Promise<void>) | undefined,
readonly extensionCommandsOnly: boolean = false,
) { }
}
@@ -97,6 +100,10 @@ const CODICON_SIZE_PX = 16;
const CODICON_PADDING_PX = 2;
export class InlineEditsGutterIndicator extends Disposable {
private readonly _onDidCloseWithCommand = this._register(new Emitter<string>());
readonly onDidCloseWithCommand: Event<string> = this._onDidCloseWithCommand.event;
constructor(
private readonly _editorObs: ObservableCodeEditor,
private readonly _data: IObservable<InlineEditsGutterIndicatorData | undefined>,
@@ -472,10 +479,13 @@ export class InlineEditsGutterIndicator extends Disposable {
GutterIndicatorMenuContent,
this._editorObs,
data.gutterMenuData,
(focusEditor) => {
(focusEditor, commandId) => {
if (focusEditor) {
this._editorObs.editor.focus();
}
if (commandId) {
this._onDidCloseWithCommand.fire(commandId);
}
h?.dispose();
},
).toDisposableLiveElement());
@@ -584,6 +594,7 @@ export class InlineEditsGutterIndicator extends Disposable {
width: layout.map(l => l.iconRect.width),
position: 'relative',
right: layout.map(l => l.iconDirection === 'top' ? '1px' : '0'),
color: this._data.map(d => d?.customization?.icon?.color ? asCssVariable(d.customization.icon.color.id) : undefined),
}
}, [
layout.map((l, reader) => withStyles(renderIcon(l.icon.read(reader)), { fontSize: toPx(Math.min(l.iconRect.width - CODICON_PADDING_PX, CODICON_SIZE_PX)) })),
@@ -217,6 +217,10 @@
.monaco-keybinding-key {
font-size: 13px;
opacity: 0.7;
padding: 0;
border: none;
margin: 0;
min-width: unset;
}
&.active {
@@ -270,4 +274,3 @@
background-position: center;
background-repeat: no-repeat;
}
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as dom from '../../../base/browser/dom.js';
import { ActionBar } from '../../../base/browser/ui/actionbar/actionbar.js';
import { Button } from '../../../base/browser/ui/button/button.js';
import { KeybindingLabel } from '../../../base/browser/ui/keybindingLabel/keybindingLabel.js';
import { IListEvent, IListMouseEvent, IListRenderer, IListVirtualDelegate } from '../../../base/browser/ui/list/list.js';
import { IListAccessibilityProvider, List } from '../../../base/browser/ui/list/listWidget.js';
@@ -18,7 +19,7 @@ import './actionWidget.css';
import { localize } from '../../../nls.js';
import { IContextViewService } from '../../contextview/browser/contextView.js';
import { IKeybindingService } from '../../keybinding/common/keybinding.js';
import { defaultListStyles } from '../../theme/browser/defaultStyles.js';
import { defaultButtonStyles, defaultListStyles } from '../../theme/browser/defaultStyles.js';
import { asCssVariable } from '../../theme/common/colorRegistry.js';
import { ILayoutService } from '../../layout/browser/layoutService.js';
import { IHoverService } from '../../hover/browser/hover.js';
@@ -67,16 +68,42 @@ export interface IActionListItem<T> {
* Optional toolbar actions shown when the item is focused or hovered.
*/
readonly toolbarActions?: IAction[];
/**
* Optional section identifier. Items with the same section belong to the same
* collapsible group. Only meaningful when the ActionList is created with
* collapsible sections.
*/
readonly section?: string;
/**
* When true, clicking this item toggles the section's collapsed state
* instead of selecting it.
*/
readonly isSectionToggle?: boolean;
/**
* Optional CSS class name to add to the row container.
*/
readonly className?: string;
/**
* Optional badge text to display after the label (e.g., "New").
*/
readonly badge?: string;
/**
* When set, the description is rendered as a primary button.
* The callback is invoked when the button is clicked.
*/
readonly descriptionButton?: { readonly label: string; readonly onDidClick: () => void };
}
interface IActionMenuTemplateData {
readonly container: HTMLElement;
readonly icon: HTMLElement;
readonly text: HTMLElement;
readonly badge: HTMLElement;
readonly description?: HTMLElement;
readonly keybinding: KeybindingLabel;
readonly toolbar: HTMLElement;
readonly elementDisposables: DisposableStore;
previousClassName?: string;
}
export const enum ActionListItemKind {
@@ -159,6 +186,10 @@ class ActionItemRenderer<T> implements IListRenderer<IActionListItem<T>, IAction
text.className = 'title';
container.append(text);
const badge = document.createElement('span');
badge.className = 'action-item-badge';
container.append(badge);
const description = document.createElement('span');
description.className = 'description';
container.append(description);
@@ -171,7 +202,7 @@ class ActionItemRenderer<T> implements IListRenderer<IActionListItem<T>, IAction
const elementDisposables = new DisposableStore();
return { container, icon, text, description, keybinding, toolbar, elementDisposables };
return { container, icon, text, badge, description, keybinding, toolbar, elementDisposables };
}
renderElement(element: IActionListItem<T>, _index: number, data: IActionMenuTemplateData): void {
@@ -194,10 +225,40 @@ class ActionItemRenderer<T> implements IListRenderer<IActionListItem<T>, IAction
dom.setVisibility(!element.hideIcon, data.icon);
// Apply optional className - clean up previous to avoid stale classes
// from virtualized row reuse
if (data.previousClassName) {
data.container.classList.remove(data.previousClassName);
}
data.container.classList.toggle('action-list-custom', !!element.className);
if (element.className) {
data.container.classList.add(element.className);
}
data.previousClassName = element.className;
data.text.textContent = stripNewlines(element.label);
// Render optional badge
if (element.badge) {
data.badge.textContent = element.badge;
data.badge.style.display = '';
} else {
data.badge.textContent = '';
data.badge.style.display = 'none';
}
// if there is a keybinding, prioritize over description for now
if (element.keybinding) {
if (element.descriptionButton) {
data.description!.textContent = '';
data.description!.style.display = 'inline';
const button = new Button(data.description!, { ...defaultButtonStyles, small: true });
button.label = element.descriptionButton.label;
data.elementDisposables.add(button.onDidClick(e => {
e?.stopPropagation();
element.descriptionButton!.onDidClick();
}));
data.elementDisposables.add(button);
} else if (element.keybinding) {
data.description!.textContent = element.keybinding.getLabel();
data.description!.style.display = 'inline';
data.description!.style.letterSpacing = '0.5px';
@@ -261,6 +322,26 @@ function getKeyboardNavigationLabel<T>(item: IActionListItem<T>): string | undef
return undefined;
}
/**
* Options for configuring the action list.
*/
export interface IActionListOptions {
/**
* When true, shows a filter input at the bottom of the list.
*/
readonly showFilter?: boolean;
/**
* Section IDs that should be collapsed by default.
*/
readonly collapsedByDefault?: ReadonlySet<string>;
/**
* Minimum width for the action list.
*/
readonly minWidth?: number;
}
export class ActionList<T> extends Disposable {
public readonly domNode: HTMLElement;
@@ -277,12 +358,20 @@ export class ActionList<T> extends Disposable {
private _hover = this._register(new MutableDisposable<IHoverWidget>());
private readonly _collapsedSections = new Set<string>();
private _filterText = '';
private readonly _filterInput: HTMLInputElement | undefined;
private readonly _filterContainer: HTMLElement | undefined;
private _lastMinWidth = 0;
private _hasLaidOut = false;
constructor(
user: string,
preview: boolean,
items: readonly IActionListItem<T>[],
private readonly _delegate: IActionListDelegate<T>,
accessibilityProvider: Partial<IListAccessibilityProvider<IActionListItem<T>>> | undefined,
private readonly _options: IActionListOptions | undefined,
@IContextViewService private readonly _contextViewService: IContextViewService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@ILayoutService private readonly _layoutService: ILayoutService,
@@ -291,6 +380,14 @@ export class ActionList<T> extends Disposable {
super();
this.domNode = document.createElement('div');
this.domNode.classList.add('actionList');
// Initialize collapsed sections
if (this._options?.collapsedByDefault) {
for (const section of this._options.collapsedByDefault) {
this._collapsedSections.add(section);
}
}
const virtualDelegate: IListVirtualDelegate<IActionListItem<T>> = {
getHeight: element => {
switch (element.kind) {
@@ -312,7 +409,7 @@ export class ActionList<T> extends Disposable {
new SeparatorRenderer(),
], {
keyboardSupport: false,
typeNavigationEnabled: true,
typeNavigationEnabled: !this._options?.showFilter,
keyboardNavigationLabelProvider: { getKeyboardNavigationLabel },
accessibilityProvider: {
getAriaLabel: element => {
@@ -352,13 +449,151 @@ export class ActionList<T> extends Disposable {
this._register(this._list.onDidChangeSelection(e => this.onListSelection(e)));
this._allMenuItems = items;
this._list.splice(0, this._list.length, this._allMenuItems);
// Create filter input
if (this._options?.showFilter) {
this._filterContainer = document.createElement('div');
this._filterContainer.className = 'action-list-filter';
this._filterInput = document.createElement('input');
this._filterInput.type = 'text';
this._filterInput.className = 'action-list-filter-input';
this._filterInput.placeholder = localize('actionList.filter.placeholder', "Search...");
this._filterInput.setAttribute('aria-label', localize('actionList.filter.ariaLabel', "Filter items"));
this._filterContainer.appendChild(this._filterInput);
this._register(dom.addDisposableListener(this._filterInput, 'input', () => {
this._filterText = this._filterInput!.value;
this._applyFilter();
}));
// Keyboard navigation from filter input
this._register(dom.addDisposableListener(this._filterInput, 'keydown', (e: KeyboardEvent) => {
if (e.key === 'ArrowUp') {
e.preventDefault();
this._list.domFocus();
const lastIndex = this._list.length - 1;
if (lastIndex >= 0) {
this._list.focusLast(undefined, this.focusCondition);
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
this._list.domFocus();
this.focusNext();
} else if (e.key === 'Enter') {
e.preventDefault();
this.acceptSelected();
} else if (e.key === 'Escape') {
if (this._filterText) {
e.preventDefault();
e.stopPropagation();
this._filterInput!.value = '';
this._filterText = '';
this._applyFilter();
}
}
}));
}
this._applyFilter();
if (this._list.length) {
this.focusNext();
}
}
private _toggleSection(section: string): void {
if (this._collapsedSections.has(section)) {
this._collapsedSections.delete(section);
} else {
this._collapsedSections.add(section);
}
this._applyFilter();
}
private _applyFilter(): void {
const filterLower = this._filterText.toLowerCase();
const isFiltering = filterLower.length > 0;
const visible: IActionListItem<T>[] = [];
for (const item of this._allMenuItems) {
if (item.kind === ActionListItemKind.Header) {
if (isFiltering) {
// When filtering, skip all headers
continue;
}
visible.push(item);
continue;
}
if (item.kind === ActionListItemKind.Separator) {
if (isFiltering) {
continue;
}
visible.push(item);
continue;
}
// Action item
if (isFiltering) {
// When filtering, skip section toggle items and only match content
if (item.isSectionToggle) {
continue;
}
// Match against label and description
const label = (item.label ?? '').toLowerCase();
const desc = (item.description ?? '').toLowerCase();
if (label.includes(filterLower) || desc.includes(filterLower)) {
visible.push(item);
}
} else {
// Update icon for section toggle items based on collapsed state
if (item.isSectionToggle && item.section) {
const collapsed = this._collapsedSections.has(item.section);
visible.push({
...item,
group: { ...item.group!, icon: collapsed ? Codicon.chevronRight : Codicon.chevronDown },
});
continue;
}
// Not filtering - check collapsed sections
if (item.section && this._collapsedSections.has(item.section)) {
continue;
}
visible.push(item);
}
}
// Capture whether the filter input currently has focus before splice
// which may cause DOM changes that shift focus.
const filterInputHasFocus = this._filterInput && dom.isActiveElement(this._filterInput);
this._list.splice(0, this._list.length, visible);
// Re-layout to adjust height after items changed
if (this._hasLaidOut) {
this.layout(this._lastMinWidth);
// Restore focus after splice destroyed DOM elements,
// otherwise the blur handler in ActionWidgetService closes the widget.
// Keep focus on the filter input if the user is typing a filter.
if (filterInputHasFocus) {
this._filterInput!.focus();
} else {
this._list.domFocus();
}
// Reposition the context view so the widget grows in the correct direction
this._contextViewService.layout();
}
}
/**
* Returns the filter container element, if filter is enabled.
* The caller is responsible for appending it to the widget DOM.
*/
get filterContainer(): HTMLElement | undefined {
return this._filterContainer;
}
private focusCondition(element: IActionListItem<unknown>): boolean {
return !element.disabled && element.kind === ActionListItemKind.Action;
}
@@ -371,39 +606,57 @@ export class ActionList<T> extends Disposable {
}
layout(minWidth: number): number {
// Updating list height, depending on how many separators and headers there are.
const numHeaders = this._allMenuItems.filter(item => item.kind === 'header').length;
const numSeparators = this._allMenuItems.filter(item => item.kind === 'separator').length;
const itemsHeight = this._allMenuItems.length * this._actionLineHeight;
const heightWithHeaders = itemsHeight + numHeaders * this._headerLineHeight - numHeaders * this._actionLineHeight;
const heightWithSeparators = heightWithHeaders + numSeparators * this._separatorLineHeight - numSeparators * this._actionLineHeight;
this._list.layout(heightWithSeparators);
let maxWidth = minWidth;
this._hasLaidOut = true;
this._lastMinWidth = minWidth;
// Compute height based on currently visible items in the list
const visibleCount = this._list.length;
let listHeight = 0;
for (let i = 0; i < visibleCount; i++) {
const element = this._list.element(i);
switch (element.kind) {
case ActionListItemKind.Header:
listHeight += this._headerLineHeight;
break;
case ActionListItemKind.Separator:
listHeight += this._separatorLineHeight;
break;
default:
listHeight += this._actionLineHeight;
break;
}
}
if (this._allMenuItems.length >= 50) {
maxWidth = 380;
this._list.layout(listHeight);
const effectiveMinWidth = Math.max(minWidth, this._options?.minWidth ?? 0);
let maxWidth = effectiveMinWidth;
if (visibleCount >= 50) {
maxWidth = Math.max(380, effectiveMinWidth);
} else {
// For finding width dynamically (not using resize observer)
const itemWidths: number[] = this._allMenuItems.map((_, index): number => {
const element = this._getRowElement(index);
const itemWidths: number[] = [];
for (let i = 0; i < visibleCount; i++) {
const element = this._getRowElement(i);
if (element) {
element.style.width = 'auto';
const width = element.getBoundingClientRect().width;
element.style.width = '';
return width;
itemWidths.push(width);
}
return 0;
});
}
// resize observer - can be used in the future since list widget supports dynamic height but not width
maxWidth = Math.max(...itemWidths, minWidth);
maxWidth = Math.max(...itemWidths, effectiveMinWidth);
}
const filterHeight = this._filterContainer ? 36 : 0;
const maxVhPrecentage = 0.7;
const height = Math.min(heightWithSeparators, this._layoutService.getContainer(dom.getWindow(this.domNode)).clientHeight * maxVhPrecentage);
this._list.layout(height, maxWidth);
const maxHeight = this._layoutService.getContainer(dom.getWindow(this.domNode)).clientHeight * maxVhPrecentage;
const height = Math.min(listHeight + filterHeight, maxHeight);
const listFinalHeight = height - filterHeight;
this._list.layout(listFinalHeight, maxWidth);
this.domNode.style.height = `${height}px`;
this.domNode.style.height = `${listFinalHeight}px`;
this._list.domFocus();
return maxWidth;
@@ -447,6 +700,10 @@ export class ActionList<T> extends Disposable {
}
const element = e.elements[0];
if (element.isSectionToggle) {
this._list.setSelection([]);
return;
}
if (element.item && this.focusCondition(element)) {
this._delegate.onSelect(element.item, e.browserEvent instanceof PreviewSelectedEvent);
} else {
@@ -526,6 +783,11 @@ export class ActionList<T> extends Disposable {
}
private onListClick(e: IListMouseEvent<IActionListItem<T>>): void {
if (e.element && e.element.isSectionToggle && e.element.section) {
const section = e.element.section;
queueMicrotask(() => this._toggleSection(section));
return;
}
if (e.element && this.focusCondition(e.element)) {
this._list.setFocus([]);
}
@@ -122,6 +122,7 @@
display: flex;
gap: 6px;
align-items: center;
color: var(--vscode-foreground) !important;
}
.action-widget .monaco-list-row.action .codicon {
@@ -150,6 +151,16 @@
text-overflow: ellipsis;
}
.action-widget .monaco-list-row.action .action-item-badge {
padding: 0px 6px;
border-radius: 10px;
background-color: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
font-size: 11px;
line-height: 18px;
flex-shrink: 0;
}
.action-widget .monaco-list-row.action .monaco-keybinding > .monaco-keybinding-key {
background-color: var(--vscode-keybindingLabel-background);
color: var(--vscode-keybindingLabel-foreground);
@@ -205,8 +216,10 @@
.action-widget .monaco-list .monaco-list-row .description {
opacity: 0.7;
margin-left: 0.5em;
flex-shrink: 0;
}
/* Item toolbar - shows on hover/focus */
.action-widget .monaco-list-row.action .action-list-item-toolbar {
display: none;
@@ -227,3 +240,29 @@
gap: 4px;
font-size: 12px;
}
/* Filter input */
.action-widget .action-list-filter {
border-top: 1px solid var(--vscode-editorHoverWidget-border);
padding: 4px;
}
.action-widget .action-list-filter-input {
width: 100%;
box-sizing: border-box;
padding: 4px 8px;
border: 1px solid var(--vscode-input-border, transparent);
border-radius: 3px;
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
font-size: 12px;
outline: none;
}
.action-widget .action-list-filter-input:focus {
border-color: var(--vscode-focusBorder);
}
.action-widget .action-list-filter-input::placeholder {
color: var(--vscode-input-placeholderForeground);
}
@@ -10,7 +10,7 @@ import { KeyCode, KeyMod } from '../../../base/common/keyCodes.js';
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js';
import './actionWidget.css';
import { localize, localize2 } from '../../../nls.js';
import { acceptSelectedActionCommand, ActionList, IActionListDelegate, IActionListItem, previewSelectedActionCommand } from './actionList.js';
import { acceptSelectedActionCommand, ActionList, IActionListDelegate, IActionListItem, IActionListOptions, previewSelectedActionCommand } from './actionList.js';
import { Action2, registerAction2 } from '../../actions/common/actions.js';
import { IContextKeyService, RawContextKey } from '../../contextkey/common/contextkey.js';
import { IContextViewService } from '../../contextview/browser/contextView.js';
@@ -36,7 +36,7 @@ export const IActionWidgetService = createDecorator<IActionWidgetService>('actio
export interface IActionWidgetService {
readonly _serviceBrand: undefined;
show<T>(user: string, supportsPreview: boolean, items: readonly IActionListItem<T>[], delegate: IActionListDelegate<T>, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[], accessibilityProvider?: Partial<IListAccessibilityProvider<IActionListItem<T>>>): void;
show<T>(user: string, supportsPreview: boolean, items: readonly IActionListItem<T>[], delegate: IActionListDelegate<T>, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[], accessibilityProvider?: Partial<IListAccessibilityProvider<IActionListItem<T>>>, listOptions?: IActionListOptions): void;
hide(didCancel?: boolean): void;
@@ -60,10 +60,10 @@ class ActionWidgetService extends Disposable implements IActionWidgetService {
super();
}
show<T>(user: string, supportsPreview: boolean, items: readonly IActionListItem<T>[], delegate: IActionListDelegate<T>, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[], accessibilityProvider?: Partial<IListAccessibilityProvider<IActionListItem<T>>>): void {
show<T>(user: string, supportsPreview: boolean, items: readonly IActionListItem<T>[], delegate: IActionListDelegate<T>, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[], accessibilityProvider?: Partial<IListAccessibilityProvider<IActionListItem<T>>>, listOptions?: IActionListOptions): void {
const visibleContext = ActionWidgetContextKeys.Visible.bindTo(this._contextKeyService);
const list = this._instantiationService.createInstance(ActionList, user, supportsPreview, items, delegate, accessibilityProvider);
const list = this._instantiationService.createInstance(ActionList, user, supportsPreview, items, delegate, accessibilityProvider, listOptions);
this._contextViewService.showContextView({
getAnchor: () => anchor,
render: (container: HTMLElement) => {
@@ -137,6 +137,11 @@ class ActionWidgetService extends Disposable implements IActionWidgetService {
}
}
// Filter input (appended after the list, before action bar visually)
if (this._list.value?.filterContainer) {
widget.appendChild(this._list.value.filterContainer);
}
const width = this._list.value?.layout(actionBarWidth);
widget.style.width = `${width}px`;
@@ -6,7 +6,7 @@
import { IActionWidgetService } from './actionWidget.js';
import { IAction } from '../../../base/common/actions.js';
import { BaseDropdown, IActionProvider, IBaseDropdownOptions } from '../../../base/browser/ui/dropdown/dropdown.js';
import { ActionListItemKind, IActionListDelegate, IActionListItem, IActionListItemHover } from './actionList.js';
import { ActionListItemKind, IActionListDelegate, IActionListItem, IActionListItemHover, IActionListOptions } from './actionList.js';
import { ThemeIcon } from '../../../base/common/themables.js';
import { Codicon } from '../../../base/common/codicons.js';
import { getActiveElement, isHTMLElement } from '../../../base/browser/dom.js';
@@ -52,6 +52,11 @@ export interface IActionWidgetDropdownOptions extends IBaseDropdownOptions {
* provided, no telemetry will be sent.
*/
readonly reporter?: { id: string; name?: string; includeOptions?: boolean };
/**
* Options for the underlying ActionList (filter, collapsible sections).
*/
readonly listOptions?: IActionListOptions;
}
/**
@@ -201,7 +206,8 @@ export class ActionWidgetDropdown extends BaseDropdown {
this._options.getAnchor?.() ?? this.element,
undefined,
actionBarActions,
accessibilityProvider
accessibilityProvider,
this._options.listOptions
);
}
+2 -1
View File
@@ -281,10 +281,11 @@ export class MenuId {
static readonly ChatToolOutputResourceContext = new MenuId('ChatToolOutputResourceContext');
static readonly ChatMultiDiffContext = new MenuId('ChatMultiDiffContext');
static readonly ChatConfirmationMenu = new MenuId('ChatConfirmationMenu');
static readonly ChatEditorInlineGutter = new MenuId('ChatEditorInlineGutter');
static readonly ChatEditorInlineMenu = new MenuId('ChatEditorInlineGutter');
static readonly ChatEditorInlineExecute = new MenuId('ChatEditorInputExecute');
static readonly ChatEditorInlineInputSide = new MenuId('ChatEditorInputSide');
static readonly InlineChatEditorAffordance = new MenuId('InlineChatEditorAffordance');
static readonly InlineChatInput = new MenuId('InlineChatInput');
static readonly AccessibleView = new MenuId('AccessibleView');
static readonly MultiDiffEditorContent = new MenuId('MultiDiffEditorContent');
static readonly MultiDiffEditorFileToolbar = new MenuId('MultiDiffEditorFileToolbar');
@@ -46,6 +46,10 @@ export interface INativeBrowserElementsService {
getElementData(rect: IRectangle, token: CancellationToken, locator: IBrowserTargetLocator, cancellationId?: number): Promise<IElementData | undefined>;
startDebugSession(token: CancellationToken, locator: IBrowserTargetLocator, cancelAndDetachId?: number): Promise<void>;
startConsoleSession(token: CancellationToken, locator: IBrowserTargetLocator, cancelAndDetachId?: number): Promise<void>;
getConsoleLogs(locator: IBrowserTargetLocator): Promise<string | undefined>;
}
/**
@@ -25,6 +25,17 @@ interface NodeDataResponse {
bounds: IRectangle;
}
const MAX_CONSOLE_LOG_ENTRIES = 1000;
const consoleLogStore = new Map<string, string[]>();
function locatorKey(locator: IBrowserTargetLocator): string {
const key = locator.browserViewId ?? locator.webviewId;
if (!key) {
return 'unknown';
}
return key;
}
export class NativeBrowserElementsMainService extends Disposable implements INativeBrowserElementsMainService {
_serviceBrand: undefined;
@@ -38,11 +49,82 @@ export class NativeBrowserElementsMainService extends Disposable implements INat
get windowId(): never { throw new Error('Not implemented in electron-main'); }
async getConsoleLogs(windowId: number | undefined, locator: IBrowserTargetLocator): Promise<string | undefined> {
const key = locatorKey(locator);
const entries = consoleLogStore.get(key);
if (!entries || entries.length === 0) {
return undefined;
}
return entries.join('\n');
}
async startConsoleSession(windowId: number | undefined, token: CancellationToken, locator: IBrowserTargetLocator, cancelAndDetachId?: number): Promise<void> {
const window = this.windowById(windowId);
if (!window?.win) {
return undefined;
}
let targetWebContents: Electron.WebContents | undefined;
if (locator.browserViewId) {
targetWebContents = this.browserViewMainService.tryGetBrowserView(locator.browserViewId)?.webContents;
}
if (!targetWebContents) {
return undefined;
}
const key = locatorKey(locator);
if (!consoleLogStore.has(key)) {
consoleLogStore.set(key, []);
}
const levelMap: Record<number, string> = { 0: 'log', 1: 'warning', 2: 'error' };
const onConsoleMessage = (_event: Electron.Event, level: number, message: string, _line: number, _sourceId: string) => {
const levelName = levelMap[level] ?? 'log';
const formatted = `[${levelName}] ${message}`;
const current = consoleLogStore.get(key) ?? [];
current.push(formatted);
if (current.length > MAX_CONSOLE_LOG_ENTRIES) {
current.splice(0, current.length - MAX_CONSOLE_LOG_ENTRIES);
}
consoleLogStore.set(key, current);
};
const cleanupListeners = () => {
targetWebContents?.off('console-message', onConsoleMessage);
window.win?.webContents.off('ipc-message', onIpcMessage);
};
const onIpcMessage = async (_event: Electron.Event, channel: string, closedCancelAndDetachId: number) => {
if (channel === `vscode:cancelConsoleSession${cancelAndDetachId}`) {
if (cancelAndDetachId !== closedCancelAndDetachId) {
return;
}
cleanupListeners();
consoleLogStore.delete(key);
}
};
targetWebContents.on('console-message', onConsoleMessage);
targetWebContents.once('destroyed', () => {
cleanupListeners();
consoleLogStore.delete(key);
});
token.onCancellationRequested(() => {
cleanupListeners();
consoleLogStore.delete(key);
});
window.win.webContents.on('ipc-message', onIpcMessage);
}
/**
* Find the webview target that matches the given locator.
* Checks either webviewId or browserViewId depending on what's provided.
*/
async findWebviewTarget(debuggers: Electron.Debugger, locator: IBrowserTargetLocator): Promise<string | undefined> {
private async findWebviewTarget(debuggers: Electron.Debugger, locator: IBrowserTargetLocator): Promise<string | undefined> {
const { targetInfos } = await debuggers.sendCommand('Target.getTargets');
if (locator.webviewId) {
@@ -275,9 +275,4 @@ export interface IBrowserViewService {
* @param id The browser view identifier
*/
clearStorage(id: string): Promise<void>;
/**
* Get a CDP WebSocket endpoint URL.
*/
getDebugWebSocketEndpoint(): Promise<string>;
}
@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from '../../../base/common/event.js';
import { IDisposable } from '../../../base/common/lifecycle.js';
export const ipcBrowserViewGroupChannelName = 'browserViewGroup';
/**
* Fired when a browser view is added to or removed from a group.
*/
export interface IBrowserViewGroupViewEvent {
/** The ID of the browser view that was added or removed. */
readonly viewId: string;
}
/**
* A browser view group - an isolated collection of browser views.
*
* This interface is shared between the main-process entity and remote proxies.
*/
export interface IBrowserViewGroup extends IDisposable {
readonly id: string;
readonly onDidAddView: Event<IBrowserViewGroupViewEvent>;
readonly onDidRemoveView: Event<IBrowserViewGroupViewEvent>;
readonly onDidDestroy: Event<void>;
addView(viewId: string): Promise<void>;
removeView(viewId: string): Promise<void>;
getDebugWebSocketEndpoint(): Promise<string>;
}
/**
* Common service for managing browser view groups across processes.
*
* A browser view group is an isolated collection of browser views that can be
* independently exposed to different services or CDP clients.
*
* This interface is consumed via {@link ProxyChannel}.
* The main-process implementation is {@link BrowserViewGroupMainService}.
*/
export interface IBrowserViewGroupService {
// Dynamic events - one per group instance, keyed by group ID.
onDynamicDidAddView(groupId: string): Event<IBrowserViewGroupViewEvent>;
onDynamicDidRemoveView(groupId: string): Event<IBrowserViewGroupViewEvent>;
onDynamicDidDestroy(groupId: string): Event<void>;
/**
* Create a new browser view group.
* @returns The id of the newly created group.
*/
createGroup(): Promise<string>;
/**
* Destroy a browser view group.
* Views in the group are **not** destroyed - they are simply detached.
* @param groupId The group identifier.
*/
destroyGroup(groupId: string): Promise<void>;
/**
* Add a browser view to a group.
* A view can belong to multiple groups simultaneously.
* @param groupId The group identifier.
* @param viewId The browser view identifier.
*/
addViewToGroup(groupId: string, viewId: string): Promise<void>;
/**
* Remove a browser view from a group.
* @param groupId The group identifier.
* @param viewId The browser view identifier.
*/
removeViewFromGroup(groupId: string, viewId: string): Promise<void>;
/**
* Get a short-lived CDP WebSocket endpoint URL for a specific group.
* The returned URL contains a single-use token.
* @param groupId The group identifier.
*/
getDebugWebSocketEndpoint(groupId: string): Promise<string>;
}
@@ -22,44 +22,60 @@ export interface IBrowserViewCDPProxyServer {
readonly _serviceBrand: undefined;
/**
* Returns a debug endpoint with a short-lived, single-use token.
* Returns a debug endpoint with a short-lived, single-use token for a specific browser target.
*/
getWebSocketEndpoint(): Promise<string>;
getWebSocketEndpointForTarget(target: ICDPBrowserTarget): Promise<string>;
/**
* Unregister a previously registered browser target.
*/
removeTarget(target: ICDPBrowserTarget): Promise<void>;
}
/**
* WebSocket server that provides CDP debugging for browser views.
*
* Manages a registry of {@link ICDPBrowserTarget} instances, each reachable
* at its own `/devtools/browser/{id}` WebSocket endpoint.
*/
export class BrowserViewCDPProxyServer extends Disposable implements IBrowserViewCDPProxyServer {
declare readonly _serviceBrand: undefined;
private server: http.Server | undefined;
private port: number | undefined;
private readonly tokens: TokenManager;
private readonly tokens = this._register(new TokenManager<string>());
private readonly targets = new Map<string, ICDPBrowserTarget>();
constructor(
private readonly browserTarget: ICDPBrowserTarget,
@ILogService private readonly logService: ILogService
) {
super();
this.tokens = this._register(new TokenManager());
}
/**
* Returns a debug endpoint with a short-lived, single-use token in the
* WebSocket URL. The token is revoked once a WebSocket connection is made
* or after 30 seconds, whichever comes first.
* Register a browser target and return a WebSocket endpoint URL for it.
* The target is reachable at `/devtools/browser/{targetId}`.
*/
async getWebSocketEndpoint(): Promise<string> {
async getWebSocketEndpointForTarget(target: ICDPBrowserTarget): Promise<string> {
await this.ensureServerStarted();
const token = await this.tokens.issueToken();
return this.getWebSocketUrl(token);
const targetInfo = await target.getTargetInfo();
const targetId = targetInfo.targetId;
// Register (or re-register) the target
this.targets.set(targetId, target);
const token = await this.tokens.issueToken(targetId);
return `ws://localhost:${this.port}/devtools/browser/${targetId}?token=${token}`;
}
private getWebSocketUrl(token: string): string {
return `ws://localhost:${this.port}/devtools/browser?token=${token}`;
/**
* Unregister a previously registered browser target.
*/
async removeTarget(target: ICDPBrowserTarget): Promise<void> {
const targetInfo = await target.getTargetInfo();
this.targets.delete(targetInfo.targetId);
}
private async ensureServerStarted(): Promise<void> {
@@ -93,14 +109,7 @@ export class BrowserViewCDPProxyServer extends Disposable implements IBrowserVie
private handleWebSocketUpgrade(req: http.IncomingMessage, socket: Socket): void {
const [pathname, params] = (req.url || '').split('?');
const token = new URLSearchParams(params).get('token');
if (!token || !this.tokens.consumeToken(token)) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.end();
return;
}
const browserMatch = pathname.match(/^\/devtools\/browser(\/.*)?$/);
const browserMatch = pathname.match(/^\/devtools\/browser\/([^/?]+)$/);
this.logService.debug(`[BrowserViewDebugProxy] WebSocket upgrade requested: ${pathname}`);
@@ -111,6 +120,24 @@ export class BrowserViewCDPProxyServer extends Disposable implements IBrowserVie
return;
}
const targetId = browserMatch[1];
const token = new URLSearchParams(params).get('token');
const tokenTargetId = token && this.tokens.consumeToken(token);
if (!tokenTargetId || tokenTargetId !== targetId) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.end();
return;
}
const target = this.targets.get(targetId);
if (!target) {
this.logService.warn(`[BrowserViewDebugProxy] Browser target not found: ${targetId}`);
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
socket.end();
return;
}
this.logService.debug(`[BrowserViewDebugProxy] WebSocket connected: ${pathname}`);
const upgraded = upgradeToISocket(req, socket, {
@@ -122,7 +149,7 @@ export class BrowserViewCDPProxyServer extends Disposable implements IBrowserVie
return;
}
const proxy = new CDPBrowserProxy(this.browserTarget);
const proxy = new CDPBrowserProxy(target);
const disposables = this.wireWebSocket(upgraded, proxy);
this._register(disposables);
this._register(upgraded);
@@ -200,31 +227,35 @@ export class BrowserViewCDPProxyServer extends Disposable implements IBrowserVie
}
}
class TokenManager extends Disposable {
/** Map of currently valid single-use tokens. Each expires after 30 seconds. */
private readonly tokens = new Map<string, { expiresAt: number }>();
class TokenManager<TDetails> extends Disposable {
/** Map of currently valid single-use tokens to their associated details. */
private readonly tokens = new Map<string, { details: TDetails; expiresAt: number }>();
/**
* Creates a short-lived, single-use token.
* Creates a short-lived, single-use token bound to a specific target.
* The token is revoked once consumed or after 30 seconds.
*/
async issueToken(): Promise<string> {
async issueToken(details: TDetails): Promise<string> {
const token = this.makeToken();
this.tokens.set(token, { expiresAt: Date.now() + 30_000 });
this.tokens.set(token, { details: Object.freeze(details), expiresAt: Date.now() + 30_000 });
this._register(disposableTimeout(() => this.tokens.delete(token), 30_000));
return token;
}
consumeToken(token: string): boolean {
/**
* Consume a token. Returns the details it was issued with, or
* `undefined` if the token is invalid or expired.
*/
consumeToken(token: string): TDetails | undefined {
if (!token) {
return false;
return undefined;
}
const info = this.tokens.get(token);
if (!info) {
return false;
return undefined;
}
this.tokens.delete(token);
return Date.now() <= info.expiresAt;
return Date.now() <= info.expiresAt ? info.details : undefined;
}
private makeToken(): string {
@@ -0,0 +1,202 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { BrowserView } from './browserView.js';
import { ICDPTarget, CDPBrowserVersion, CDPWindowBounds, CDPTargetInfo, ICDPConnection, ICDPBrowserTarget } from '../common/cdp/types.js';
import { CDPBrowserProxy } from '../common/cdp/proxy.js';
import { IBrowserViewGroup, IBrowserViewGroupViewEvent } from '../common/browserViewGroup.js';
import { IBrowserViewCDPProxyServer } from './browserViewCDPProxyServer.js';
import { IBrowserViewMainService } from './browserViewMainService.js';
/**
* An isolated group of {@link BrowserView} instances exposed as CDP targets.
*
* Each group represents an independent CDP "browser" endpoint
* (`/devtools/browser/{id}`). Different groups can expose different
* subsets of browser views, enabling selective target visibility across
* CDP sessions.
*
* Created via {@link BrowserViewGroupMainService.createGroup}.
*/
export class BrowserViewGroup extends Disposable implements ICDPBrowserTarget, IBrowserViewGroup {
private readonly views = new Map<string, BrowserView>();
private readonly viewListeners = this._register(new DisposableStore());
/** All context IDs known to this group, including those from views added to it. */
private readonly knownContextIds = new Set<string>();
/** Browser context IDs created by this group via {@link createBrowserContext}. */
private readonly ownedContextIds = new Set<string>();
private readonly _onTargetCreated = this._register(new Emitter<BrowserView>());
readonly onTargetCreated: Event<BrowserView> = this._onTargetCreated.event;
private readonly _onTargetDestroyed = this._register(new Emitter<BrowserView>());
readonly onTargetDestroyed: Event<BrowserView> = this._onTargetDestroyed.event;
private readonly _onDidAddView = this._register(new Emitter<IBrowserViewGroupViewEvent>());
readonly onDidAddView: Event<IBrowserViewGroupViewEvent> = this._onDidAddView.event;
private readonly _onDidRemoveView = this._register(new Emitter<IBrowserViewGroupViewEvent>());
readonly onDidRemoveView: Event<IBrowserViewGroupViewEvent> = this._onDidRemoveView.event;
private readonly _onDidDestroy = this._register(new Emitter<void>());
readonly onDidDestroy: Event<void> = this._onDidDestroy.event;
constructor(
readonly id: string,
@IBrowserViewMainService private readonly browserViewMainService: IBrowserViewMainService,
@IBrowserViewCDPProxyServer private readonly cdpProxyServer: IBrowserViewCDPProxyServer,
) {
super();
}
// #region View management
/**
* Add a {@link BrowserView} to this group.
* Fires {@link onDidAddView} and {@link onTargetCreated}.
* Automatically removes the view when it closes.
*/
async addView(viewId: string): Promise<void> {
if (this.views.has(viewId)) {
return;
}
const view = this.browserViewMainService.tryGetBrowserView(viewId);
if (!view) {
throw new Error(`Browser view ${viewId} not found`);
}
this.views.set(view.id, view);
this.knownContextIds.add(view.session.id);
this._onDidAddView.fire({ viewId: view.id });
this._onTargetCreated.fire(view);
this.viewListeners.add(Event.once(view.onDidClose)(() => {
this.removeView(viewId);
}));
}
/**
* Remove a {@link BrowserView} from this group.
* Fires {@link onDidRemoveView} and {@link onTargetDestroyed} if the view was tracked.
*/
async removeView(viewId: string): Promise<void> {
const view = this.views.get(viewId);
if (view && this.views.delete(viewId)) {
this._onDidRemoveView.fire({ viewId: view.id });
this._onTargetDestroyed.fire(view);
}
}
// #endregion
// #region ICDPBrowserTarget implementation
getVersion(): CDPBrowserVersion {
return this.browserViewMainService.getVersion();
}
getWindowForTarget(target: ICDPTarget): { windowId: number; bounds: CDPWindowBounds } {
return this.browserViewMainService.getWindowForTarget(target);
}
async attach(): Promise<ICDPConnection> {
return new CDPBrowserProxy(this);
}
async getTargetInfo(): Promise<CDPTargetInfo> {
return {
targetId: this.id,
type: 'browser',
title: this.getVersion().product,
url: '',
attached: true,
canAccessOpener: false
};
}
getTargets(): IterableIterator<BrowserView> {
return this.views.values();
}
async createTarget(url: string, browserContextId?: string): Promise<ICDPTarget> {
if (browserContextId && !this.knownContextIds.has(browserContextId)) {
throw new Error(`Unknown browser context ${browserContextId}`);
}
const target = await this.browserViewMainService.createTarget(url, browserContextId);
if (target instanceof BrowserView) {
await this.addView(target.id);
}
return target;
}
async activateTarget(target: ICDPTarget): Promise<void> {
return this.browserViewMainService.activateTarget(target);
}
async closeTarget(target: ICDPTarget): Promise<boolean> {
if (target instanceof BrowserView) {
await this.removeView(target.id);
}
return this.browserViewMainService.closeTarget(target);
}
// Browser context management
/**
* Returns only the browser context IDs that are visible to this group,
* i.e. contexts used by views currently in the group.
*/
getBrowserContexts(): string[] {
return [...this.knownContextIds];
}
async createBrowserContext(): Promise<string> {
const contextId = await this.browserViewMainService.createBrowserContext();
this.knownContextIds.add(contextId);
this.ownedContextIds.add(contextId);
return contextId;
}
async disposeBrowserContext(browserContextId: string): Promise<void> {
if (!this.ownedContextIds.has(browserContextId)) {
throw new Error('Can only dispose browser contexts created by this group');
}
// Close views in this group that belong to the context before disposing
for (const view of this.views.values()) {
if (view.session.id === browserContextId) {
await this.removeView(view.id);
}
}
this.knownContextIds.delete(browserContextId);
this.ownedContextIds.delete(browserContextId);
return this.browserViewMainService.disposeBrowserContext(browserContextId);
}
// #endregion
// #region CDP endpoint
/**
* Get a WebSocket endpoint URL for connecting to this group's CDP
* session. The URL contains a short-lived, single-use token.
*/
async getDebugWebSocketEndpoint(): Promise<string> {
return this.cdpProxyServer.getWebSocketEndpointForTarget(this);
}
// #endregion
override dispose(): void {
this._onDidDestroy.fire();
this.cdpProxyServer.removeTarget(this);
super.dispose();
}
}
@@ -0,0 +1,88 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';
import { Event } from '../../../base/common/event.js';
import { createDecorator, IInstantiationService } from '../../instantiation/common/instantiation.js';
import { generateUuid } from '../../../base/common/uuid.js';
import { IBrowserViewGroupService, IBrowserViewGroupViewEvent } from '../common/browserViewGroup.js';
import { BrowserViewGroup } from './browserViewGroup.js';
export const IBrowserViewGroupMainService = createDecorator<IBrowserViewGroupMainService>('browserViewGroupMainService');
export interface IBrowserViewGroupMainService extends IBrowserViewGroupService {
readonly _serviceBrand: undefined;
}
/**
* Main-process service that manages {@link BrowserViewGroup} instances.
*
* Implements {@link IBrowserViewGroupService} so it can be surfaced to
* the workbench/shared process via {@link ProxyChannel}.
*/
export class BrowserViewGroupMainService extends Disposable implements IBrowserViewGroupMainService {
declare readonly _serviceBrand: undefined;
private readonly groups = this._register(new DisposableMap<string, BrowserViewGroup>());
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super();
}
async createGroup(): Promise<string> {
const id = generateUuid();
const group = this.instantiationService.createInstance(BrowserViewGroup, id);
this.groups.set(id, group);
// Auto-cleanup when the group disposes itself
Event.once(group.onDidDestroy)(() => {
this.groups.deleteAndLeak(id);
});
return id;
}
async destroyGroup(groupId: string): Promise<void> {
this.groups.deleteAndDispose(groupId);
}
async addViewToGroup(groupId: string, viewId: string): Promise<void> {
return this._getGroup(groupId).addView(viewId);
}
async removeViewFromGroup(groupId: string, viewId: string): Promise<void> {
return this._getGroup(groupId).removeView(viewId);
}
async getDebugWebSocketEndpoint(groupId: string): Promise<string> {
return this._getGroup(groupId).getDebugWebSocketEndpoint();
}
onDynamicDidAddView(groupId: string): Event<IBrowserViewGroupViewEvent> {
return this._getGroup(groupId).onDidAddView;
}
onDynamicDidRemoveView(groupId: string): Event<IBrowserViewGroupViewEvent> {
return this._getGroup(groupId).onDidRemoveView;
}
onDynamicDidDestroy(groupId: string): Event<void> {
return this._getGroup(groupId).onDidDestroy;
}
/**
* Get a group or throw if not found.
*/
private _getGroup(groupId: string): BrowserViewGroup {
const group = this.groups.get(groupId);
if (!group) {
throw new Error(`Browser view group ${groupId} not found`);
}
return group;
}
}
@@ -15,7 +15,6 @@ import { generateUuid } from '../../../base/common/uuid.js';
import { BrowserViewUri } from '../common/browserViewUri.js';
import { IWindowsMainService } from '../../windows/electron-main/windows.js';
import { BrowserSession } from './browserSession.js';
import { IBrowserViewCDPProxyServer } from './browserViewCDPProxyServer.js';
import { IProductService } from '../../product/common/productService.js';
import { CDPBrowserProxy } from '../common/cdp/proxy.js';
@@ -51,8 +50,7 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
@IProductService private readonly productService: IProductService,
@IBrowserViewCDPProxyServer private readonly cdpProxyServer: IBrowserViewCDPProxyServer,
@IProductService private readonly productService: IProductService
) {
super();
}
@@ -363,8 +361,4 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa
);
await browserSession.electronSession.clearData();
}
async getDebugWebSocketEndpoint(): Promise<string> {
return this.cdpProxyServer.getWebSocketEndpoint();
}
}
@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
import { IBrowserViewGroup, IBrowserViewGroupService, IBrowserViewGroupViewEvent, ipcBrowserViewGroupChannelName } from '../common/browserViewGroup.js';
export const IBrowserViewGroupRemoteService = createDecorator<IBrowserViewGroupRemoteService>('browserViewGroupRemoteService');
/**
* Remote-process service for managing browser view groups.
*
* Connects to the main-process {@link BrowserViewGroupMainService} via
* IPC and provides {@link IBrowserViewGroup} instances for
* interacting with groups.
*
* Usable from the shared process.
*/
export interface IBrowserViewGroupRemoteService {
readonly _serviceBrand: undefined;
/**
* Create a new browser view group.
*/
createGroup(): Promise<IBrowserViewGroup>;
}
/**
* Remote proxy for a browser view group living in the main process.
*/
class RemoteBrowserViewGroup extends Disposable implements IBrowserViewGroup {
constructor(
readonly id: string,
private readonly groupService: IBrowserViewGroupService,
) {
super();
this._register(groupService.onDynamicDidDestroy(this.id)(() => {
// Avoid loops
this.dispose(true);
}));
}
get onDidAddView(): Event<IBrowserViewGroupViewEvent> {
return this.groupService.onDynamicDidAddView(this.id);
}
get onDidRemoveView(): Event<IBrowserViewGroupViewEvent> {
return this.groupService.onDynamicDidRemoveView(this.id);
}
get onDidDestroy(): Event<void> {
return this.groupService.onDynamicDidDestroy(this.id);
}
async addView(viewId: string): Promise<void> {
return this.groupService.addViewToGroup(this.id, viewId);
}
async removeView(viewId: string): Promise<void> {
return this.groupService.removeViewFromGroup(this.id, viewId);
}
async getDebugWebSocketEndpoint(): Promise<string> {
return this.groupService.getDebugWebSocketEndpoint(this.id);
}
override dispose(fromService = false): void {
if (!fromService) {
this.groupService.destroyGroup(this.id);
}
super.dispose();
}
}
export class BrowserViewGroupRemoteService implements IBrowserViewGroupRemoteService {
declare readonly _serviceBrand: undefined;
private readonly _groupService: IBrowserViewGroupService;
private readonly _groups = new Map<string, IBrowserViewGroup>();
constructor(
@IMainProcessService mainProcessService: IMainProcessService,
) {
const channel = mainProcessService.getChannel(ipcBrowserViewGroupChannelName);
this._groupService = ProxyChannel.toService<IBrowserViewGroupService>(channel);
}
async createGroup(): Promise<IBrowserViewGroup> {
const id = await this._groupService.createGroup();
return this._wrap(id);
}
private _wrap(id: string): IBrowserViewGroup {
const group = new RemoteBrowserViewGroup(id, this._groupService);
this._groups.set(id, group);
Event.once(group.onDidDestroy)(() => {
this._groups.delete(id);
});
return group;
}
}
@@ -4,14 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable } from '../../../base/common/lifecycle.js';
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
import { DeferredPromise } from '../../../base/common/async.js';
import { ILogService } from '../../log/common/log.js';
import { IBrowserViewService, ipcBrowserViewChannelName } from '../common/browserView.js';
import { IPlaywrightService } from '../common/playwrightService.js';
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
import { IBrowserViewGroupRemoteService } from '../node/browserViewGroupRemoteService.js';
import { IBrowserViewGroup } from '../common/browserViewGroup.js';
// eslint-disable-next-line local/code-import-patterns
import type { Browser } from 'playwright-core';
import type { Browser, BrowserContext, Page } from 'playwright-core';
/**
* Shared-process implementation of {@link IPlaywrightService}.
@@ -19,22 +19,22 @@ import type { Browser } from 'playwright-core';
export class PlaywrightService extends Disposable implements IPlaywrightService {
declare readonly _serviceBrand: undefined;
private readonly browserViewService: IBrowserViewService;
private _browser: Browser | undefined;
private _pages: PlaywrightPageManager | undefined;
private _initPromise: Promise<void> | undefined;
constructor(
@IMainProcessService mainProcessService: IMainProcessService,
@IBrowserViewGroupRemoteService private readonly browserViewGroupRemoteService: IBrowserViewGroupRemoteService,
@ILogService private readonly logService: ILogService,
) {
super();
const channel = mainProcessService.getChannel(ipcBrowserViewChannelName);
this.browserViewService = ProxyChannel.toService<IBrowserViewService>(channel);
}
/**
* Ensure the Playwright browser connection and page map are initialized.
*/
async initialize(): Promise<void> {
if (this._browser?.isConnected()) {
if (this._pages) {
return;
}
@@ -44,30 +44,41 @@ export class PlaywrightService extends Disposable implements IPlaywrightService
this._initPromise = (async () => {
try {
this.logService.debug('[PlaywrightService] Connecting to browser via CDP');
this.logService.debug('[PlaywrightService] Creating browser view group');
const group = this._register(await this.browserViewGroupRemoteService.createGroup());
this.logService.debug('[PlaywrightService] Connecting to browser via CDP');
const playwright = await import('playwright-core');
const endpoint = await this.browserViewService.getDebugWebSocketEndpoint();
const endpoint = await group.getDebugWebSocketEndpoint();
const browser = await playwright.chromium.connectOverCDP(endpoint);
this.logService.debug('[PlaywrightService] Connected to browser');
browser.on('disconnected', () => {
this.logService.debug('[PlaywrightService] Browser disconnected');
if (this._browser === browser) {
this._browser = undefined;
}
});
// This can happen if the service was disposed while we were waiting for the connection. In that case, clean up immediately.
if (this._initPromise === undefined) {
browser.close().catch(() => { /* ignore */ });
throw new Error('PlaywrightService was disposed during initialization');
}
const pageManager = this._register(new PlaywrightPageManager(group, browser, this.logService));
browser.on('disconnected', () => {
this.logService.debug('[PlaywrightService] Browser disconnected');
if (this._browser === browser) {
group.dispose();
pageManager.dispose();
this._browser = undefined;
this._pages = undefined;
this._initPromise = undefined;
}
});
this._browser = browser;
} finally {
this._pages = pageManager;
} catch (e) {
this._initPromise = undefined;
throw e;
}
})();
@@ -83,3 +94,272 @@ export class PlaywrightService extends Disposable implements IPlaywrightService
super.dispose();
}
}
/**
* Correlates browser view IDs with Playwright {@link Page} instances.
*
* When a browser view is added to a group, two asynchronous events follow
* through independent channels:
*
* 1. The group fires {@link IBrowserViewGroup.onDidAddView} (via IPC).
* 2. Playwright receives a CDP `Target.targetCreated` event (via WebSocket)
* and fires a `page` event on the matching {@link BrowserContext}.
*
* This class pairs the two event streams by FIFO ordering: the first view-ID
* received is matched with the first page event received.
*
* A periodic scan handles the case where Playwright creates a new
* {@link BrowserContext} for a target whose session was previously unknown.
*/
class PlaywrightPageManager extends Disposable {
private readonly _viewIdToPage = new Map<string, Page>();
private readonly _pageToViewId = new WeakMap<Page, string>();
/** View IDs received from the group but not yet matched with a page. */
private _viewIdQueue: Array<{
viewId: string;
page: DeferredPromise<Page>;
}> = [];
/** Pages received from Playwright but not yet matched with a view ID. */
private _pageQueue: Array<{
page: Page;
viewId: DeferredPromise<string>;
}> = [];
private readonly _watchedContexts = new WeakSet<BrowserContext>();
private _scanTimer: ReturnType<typeof setInterval> | undefined;
constructor(
private readonly _group: IBrowserViewGroup,
private readonly _browser: Browser,
private readonly logService: ILogService,
) {
super();
this._register(_group.onDidAddView(e => this.onViewAdded(e.viewId)));
this._register(_group.onDidRemoveView(e => this.onViewRemoved(e.viewId)));
this.scanForNewContexts();
}
/**
* Create a new page in the browser and return its associated page and view ID.
*/
async newPage(): Promise<{ viewId: string; page: Page }> {
const page = await this._browser.newPage();
const viewId = await this.onPageAdded(page);
return { viewId, page };
}
/**
* Explicitly add an existing browser view to the CDP group.
*/
async addPage(viewId: string): Promise<void> {
if (this._viewIdToPage.has(viewId)) {
return;
}
if (this._viewIdQueue.some(item => item.viewId === viewId)) {
return;
}
// ensure the viewId is queued so we can immediately fetch the promise via getPage().
this.onViewAdded(viewId);
try {
await this._group.addView(viewId);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
this.logService.error('[PlaywrightPageMap] Failed to add view:', errorMessage);
this.onViewRemoved(viewId);
}
}
/**
* Remove a browser view from the CDP group.
*/
async removePage(viewId: string): Promise<void> {
this.onViewRemoved(viewId);
try {
await this._group.removeView(viewId);
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
this.logService.error('[PlaywrightPageMap] Failed to remove view:', errorMessage);
}
}
/**
* Get the Playwright {@link Page} for a browser view that has already been added.
* Throws if the view has not been added.
*/
async getPage(viewId: string): Promise<Page> {
const resolved = this._viewIdToPage.get(viewId);
if (resolved) {
return resolved;
}
const queued = this._viewIdQueue.find(item => item.viewId === viewId);
if (queued) {
return queued.page.p;
}
throw new Error(`Page "${viewId}" has not been added to the Playwright service`);
}
/**
* Called when the group fires onDidAddView. Creates a deferred entry in
* the view ID queue and attempts to match it with a page.
*/
private onViewAdded(viewId: string, timeoutMs = 10000): Promise<Page> {
const resolved = this._viewIdToPage.get(viewId);
if (resolved) {
return Promise.resolve(resolved);
}
const queued = this._viewIdQueue.find(item => item.viewId === viewId);
if (queued) {
return queued.page.p;
}
const deferred = new DeferredPromise<Page>();
const timeout = setTimeout(() => deferred.error(new Error(`Timed out waiting for page`)), timeoutMs);
deferred.p.finally(() => {
clearTimeout(timeout);
this._viewIdQueue = this._viewIdQueue.filter(item => item.viewId !== viewId);
if (this._viewIdQueue.length === 0) {
this.stopScanning();
}
});
this._viewIdQueue.push({ viewId, page: deferred });
this.tryMatch();
this.ensureScanning();
return deferred.p;
}
private onViewRemoved(viewId: string): void {
this._viewIdQueue = this._viewIdQueue.filter(item => item.viewId !== viewId);
const page = this._viewIdToPage.get(viewId);
if (page) {
this._pageToViewId.delete(page);
}
this._viewIdToPage.delete(viewId);
}
private onPageAdded(page: Page, timeoutMs = 10000): Promise<string> {
const resolved = this._pageToViewId.get(page);
if (resolved) {
return Promise.resolve(resolved);
}
const queued = this._pageQueue.find(item => item.page === page);
if (queued) {
return queued.viewId.p;
}
this.onContextAdded(page.context());
page.once('close', () => this.onPageRemoved(page));
const deferred = new DeferredPromise<string>();
const timeout = setTimeout(() => deferred.error(new Error(`Timed out waiting for browser view`)), timeoutMs);
deferred.p.finally(() => {
clearTimeout(timeout);
this._pageQueue = this._pageQueue.filter(item => item.page !== page);
});
this._pageQueue.push({ page, viewId: deferred });
this.tryMatch();
return deferred.p;
}
private onPageRemoved(page: Page): void {
this._pageQueue = this._pageQueue.filter(item => item.page !== page);
const viewId = this._pageToViewId.get(page);
if (viewId) {
this._viewIdToPage.delete(viewId);
}
this._pageToViewId.delete(page);
}
private onContextAdded(context: BrowserContext): void {
if (this._watchedContexts.has(context)) {
return;
}
this._watchedContexts.add(context);
context.on('page', (page: Page) => this.onPageAdded(page));
context.on('close', () => this.onContextRemoved(context));
for (const page of context.pages()) {
this.onPageAdded(page);
}
}
private onContextRemoved(context: BrowserContext): void {
this._watchedContexts.delete(context);
}
// --- Matching ---
/**
* Pair up queued view IDs with queued pages in FIFO order and resolve
* any callers waiting for the matched view IDs.
*/
private tryMatch(): void {
while (this._viewIdQueue.length > 0 && this._pageQueue.length > 0) {
const viewIdItem = this._viewIdQueue.shift()!;
const pageItem = this._pageQueue.shift()!;
this._viewIdToPage.set(viewIdItem.viewId, pageItem.page);
this._pageToViewId.set(pageItem.page, viewIdItem.viewId);
viewIdItem.page.complete(pageItem.page);
pageItem.viewId.complete(viewIdItem.viewId);
this.logService.debug(`[PlaywrightPageMap] Matched view ${viewIdItem.viewId} → page`);
}
if (this._viewIdQueue.length === 0) {
this.stopScanning();
}
}
// --- Context scanning ---
/**
* Watch all current {@link BrowserContext BrowserContexts} for new pages.
* Also processes any existing pages in newly discovered contexts.
*/
private scanForNewContexts(): void {
for (const context of this._browser.contexts()) {
this.onContextAdded(context);
}
}
private ensureScanning(): void {
if (this._scanTimer === undefined) {
this._scanTimer = setInterval(() => this.scanForNewContexts(), 100);
}
}
private stopScanning(): void {
if (this._scanTimer !== undefined) {
clearInterval(this._scanTimer);
this._scanTimer = undefined;
}
}
override dispose(): void {
this.stopScanning();
for (const { page } of this._viewIdQueue) {
page.error(new Error('PlaywrightPageMap disposed'));
}
for (const { viewId } of this._pageQueue) {
viewId.error(new Error('PlaywrightPageMap disposed'));
}
this._viewIdQueue = [];
this._pageQueue = [];
super.dispose();
}
}
+3 -3
View File
@@ -339,7 +339,7 @@ export interface IPtyService {
shutdown(id: number, immediate: boolean): Promise<void>;
input(id: number, data: string): Promise<void>;
sendSignal(id: number, signal: string): Promise<void>;
resize(id: number, cols: number, rows: number): Promise<void>;
resize(id: number, cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): Promise<void>;
clearBuffer(id: number): Promise<void>;
getInitialCwd(id: number): Promise<string>;
getCwd(id: number): Promise<string>;
@@ -391,7 +391,7 @@ export interface IPtyServiceContribution {
handleProcessReady(persistentProcessId: number, process: ITerminalChildProcess): void;
handleProcessDispose(persistentProcessId: number): void;
handleProcessInput(persistentProcessId: number, data: string): void;
handleProcessResize(persistentProcessId: number, cols: number, rows: number): void;
handleProcessResize(persistentProcessId: number, cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): void;
}
export interface IPtyHostController {
@@ -810,7 +810,7 @@ export interface ITerminalChildProcess {
input(data: string): void;
sendSignal(signal: string): void;
processBinary(data: string): Promise<void>;
resize(cols: number, rows: number): void;
resize(cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): void;
clearBuffer(): void | Promise<void>;
/**
@@ -254,8 +254,8 @@ export class PtyHostService extends Disposable implements IPtyHostService {
processBinary(id: number, data: string): Promise<void> {
return this._proxy.processBinary(id, data);
}
resize(id: number, cols: number, rows: number): Promise<void> {
return this._proxy.resize(id, cols, rows);
resize(id: number, cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): Promise<void> {
return this._proxy.resize(id, cols, rows, pixelWidth, pixelHeight);
}
clearBuffer(id: number): Promise<void> {
return this._proxy.clearBuffer(id);
+5 -5
View File
@@ -455,13 +455,13 @@ export class PtyService extends Disposable implements IPtyService {
return this._throwIfNoPty(id).writeBinary(data);
}
@traceRpc
async resize(id: number, cols: number, rows: number): Promise<void> {
async resize(id: number, cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): Promise<void> {
const pty = this._throwIfNoPty(id);
if (pty) {
for (const contrib of this._contributions) {
contrib.handleProcessResize(id, cols, rows);
contrib.handleProcessResize(id, cols, rows, pixelWidth, pixelHeight);
}
pty.resize(cols, rows);
pty.resize(cols, rows, pixelWidth, pixelHeight);
}
}
@traceRpc
@@ -902,7 +902,7 @@ class PersistentTerminalProcess extends Disposable {
writeBinary(data: string): Promise<void> {
return this._terminalProcess.processBinary(data);
}
resize(cols: number, rows: number): void {
resize(cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): void {
if (this._inReplay) {
return;
}
@@ -911,7 +911,7 @@ class PersistentTerminalProcess extends Disposable {
// Buffered events should flush when a resize occurs
this._bufferer.flushBuffer(this._persistentProcessId);
return this._terminalProcess.resize(cols, rows);
return this._terminalProcess.resize(cols, rows, pixelWidth, pixelHeight);
}
async clearBuffer(): Promise<void> {
this._serializer.clearBuffer();
@@ -515,7 +515,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
}
}
resize(cols: number, rows: number): void {
resize(cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): void {
if (this._store.isDisposed) {
return;
}
@@ -537,7 +537,10 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess
this._logService.trace('node-pty.IPty#resize', cols, rows);
try {
this._ptyProcess.resize(cols, rows);
const pixelSize = pixelWidth !== undefined && pixelHeight !== undefined
? { width: pixelWidth, height: pixelHeight }
: undefined;
this._ptyProcess.resize(cols, rows, pixelSize);
} catch (e) {
// Swallow error if the pty has already exited
this._logService.trace('node-pty.IPty#resize exception ' + e.message);
+1
View File
@@ -704,6 +704,7 @@ interface IPartVisibilityState {
| Date | Change |
|------|--------|
| 2026-02-17 | Added `-webkit-app-region: drag` to sidebar title area so it can be used to drag the window; interactive children (actions, composite bar, labels) marked `no-drag`; CSS rules scoped to `.agent-sessions-workbench` in `parts/media/sidebarPart.css` |
| 2026-02-13 | Documentation sync: Updated all file names, class names, and references to match current implementation. `AgenticWorkbench``Workbench`, `AgenticSidebarPart``SidebarPart`, `AgenticAuxiliaryBarPart``AuxiliaryBarPart`, `AgenticPanelPart``PanelPart`, `agenticWorkbench.ts``workbench.ts`, `agenticWorkbenchMenus.ts``menus.ts`, `agenticLayoutActions.ts``layoutActions.ts`, `AgenticTitleBarWidget``SessionsTitleBarWidget`, `AgenticTitleBarContribution``SessionsTitleBarContribution`. Removed references to deleted files (`sidebarRevealButton.ts`, `floatingToolbar.ts`, `agentic.contributions.ts`, `agenticTitleBarWidget.ts`). Updated pane composite architecture from `SyncDescriptor`-based to `AgenticPaneCompositePartService`. Moved account widget docs from titlebar to sidebar footer. Added documentation for sidebar footer, project bar, traffic light spacer, card appearance styling, widget directory, and new contrib structure (`accountMenu/`, `chat/`, `configuration/`, `sessions/`). Updated titlebar actions to reflect Run Script split button and Open submenu. Removed Toggle Maximize panel action (no longer registered). Updated contributions section with all current contributions and their locations. |
| 2026-02-13 | Changed grid structure: sidebar now spans full window height at root level (HORIZONTAL root orientation); Titlebar moved inside right section; Grid is now `Sidebar \| [Titlebar / TopRight / Panel]` instead of `Titlebar / [Sidebar \| RightSection]`; Panel maximize now excludes both titlebar and sidebar; Floating toolbar positioning no longer depends on titlebar height |
| 2026-02-11 | Simplified titlebar: replaced `BrowserTitlebarPart`-derived implementation with standalone `TitlebarPart` using three `MenuWorkbenchToolBar` sections (left/center/right); Removed `CommandCenterControl`, `WindowTitle`, layout toolbar, and manual toolbar management; Center section uses `Menus.CommandCenter` which renders session picker via `IActionViewItemService`; Right section uses `Menus.TitleBarRight` which includes account submenu; Removed `commandCenterControl.ts` file |
+1
View File
@@ -23,4 +23,5 @@ export const Menus = {
AuxiliaryBarTitle: new MenuId('SessionsAuxiliaryBarTitle'),
AuxiliaryBarTitleLeft: new MenuId('SessionsAuxiliaryBarTitleLeft'),
SidebarFooter: new MenuId('SessionsSidebarFooter'),
SidebarCustomizations: new MenuId('SessionsSidebarCustomizations'),
} as const;
@@ -105,7 +105,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart {
{
hasTitle: true,
trailingSeparator: false,
borderWidth: () => 0,
borderWidth: () => (this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder)) ? 1 : 0,
},
AuxiliaryBarPart.activeViewSettingsKey,
ActiveAuxiliaryContext.bindTo(contextKeyService),
@@ -8,6 +8,26 @@
display: none;
}
/* Make the sidebar title area draggable to move the window */
.agent-sessions-workbench .part.sidebar > .composite.title {
position: relative;
}
.agent-sessions-workbench .part.sidebar > .composite.title > .titlebar-drag-region {
top: 0;
left: 0;
display: block;
position: absolute;
width: 100%;
height: 100%;
-webkit-app-region: drag;
}
/* Interactive elements in the title area must not be draggable */
.agent-sessions-workbench .part.sidebar > .composite.title .action-item {
-webkit-app-region: no-drag;
}
/* Sidebar Footer Container */
.monaco-workbench .part.sidebar > .sidebar-footer {
display: flex;
+7 -1
View File
@@ -99,7 +99,7 @@ export class SidebarPart extends AbstractPaneCompositePart {
) {
super(
Parts.SIDEBAR_PART,
{ hasTitle: true, trailingSeparator: false, borderWidth: () => 0 },
{ hasTitle: true, trailingSeparator: false, borderWidth: () => (this.getColor(SIDE_BAR_BORDER) || this.getColor(contrastBorder)) ? 1 : 0 },
SidebarPart.activeViewletSettingsKey,
ActiveViewletContext.bindTo(contextKeyService),
SidebarFocusContext.bindTo(contextKeyService),
@@ -134,6 +134,12 @@ export class SidebarPart extends AbstractPaneCompositePart {
protected override createTitleArea(parent: HTMLElement): HTMLElement | undefined {
const titleArea = super.createTitleArea(parent);
if (titleArea) {
// Add a drag region so the sidebar title area can be used to move the window,
// matching the titlebar's drag behavior.
prepend(titleArea, $('div.titlebar-drag-region'));
}
// macOS native: the sidebar spans full height and the traffic lights
// overlay the top-left corner. Add a fixed-width spacer inside the
// title area to push content horizontally past the traffic lights.
+30 -5
View File
@@ -203,7 +203,7 @@ export class TitlebarPart extends Part implements ITitlebarPart {
}));
// Right toolbar (driven by Menus.TitleBarRight - includes account submenu)
const rightToolbarContainer = append(this.rightContent, $('div.action-toolbar-container'));
const rightToolbarContainer = prepend(this.rightContent, $('div.action-toolbar-container'));
this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, rightToolbarContainer, Menus.TitleBarRight, {
contextMenu: Menus.TitleBarContext,
telemetrySource: 'titlePart.right',
@@ -258,8 +258,15 @@ export class TitlebarPart extends Part implements ITitlebarPart {
private lastLayoutDimension: Dimension | undefined;
get hasZoomableElements(): boolean {
return true; // sessions titlebar always has command center and toolbar actions
}
get preventZoom(): boolean {
return getZoomFactor(getWindow(this.element)) < 1;
// Prevent zooming behavior if any of the following conditions are met:
// 1. Shrinking below the window control size (zoom < 1)
// 2. No custom items are present in the title bar
return getZoomFactor(getWindow(this.element)) < 1 || !this.hasZoomableElements;
}
override layout(width: number, height: number): void {
@@ -342,6 +349,7 @@ export class AuxiliaryTitlebarPart extends TitlebarPart implements IAuxiliaryTit
constructor(
readonly container: HTMLElement,
editorGroupsContainer: IEditorGroupsContainer,
private readonly mainTitlebar: TitlebarPart,
@IContextMenuService contextMenuService: IContextMenuService,
@IConfigurationService configurationService: IConfigurationService,
@IInstantiationService instantiationService: IInstantiationService,
@@ -354,6 +362,15 @@ export class AuxiliaryTitlebarPart extends TitlebarPart implements IAuxiliaryTit
const id = AuxiliaryTitlebarPart.COUNTER++;
super(`workbench.parts.auxiliaryTitle.${id}`, getWindow(container), contextMenuService, configurationService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService);
}
override get preventZoom(): boolean {
// Prevent zooming behavior if any of the following conditions are met:
// 1. Shrinking below the window control size (zoom < 1)
// 2. No custom items are present in the main title bar
// The auxiliary title bar never contains any zoomable items itself,
// but we want to match the behavior of the main title bar.
return getZoomFactor(getWindow(this.element)) < 1 || !this.mainTitlebar.hasZoomableElements;
}
}
/**
@@ -366,17 +383,21 @@ export class TitleService extends MultiWindowParts<TitlebarPart> implements ITit
readonly mainPart: TitlebarPart;
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IInstantiationService protected readonly instantiationService: IInstantiationService,
@IStorageService storageService: IStorageService,
@IThemeService themeService: IThemeService
) {
super('workbench.agentSessionsTitleService', themeService, storageService);
this.mainPart = this._register(this.instantiationService.createInstance(MainTitlebarPart));
this.mainPart = this._register(this.createMainTitlebarPart());
this.onMenubarVisibilityChange = this.mainPart.onMenubarVisibilityChange;
this._register(this.registerPart(this.mainPart));
}
protected createMainTitlebarPart(): TitlebarPart {
return this.instantiationService.createInstance(MainTitlebarPart);
}
//#region Auxiliary Titlebar Parts
createAuxiliaryTitlebarPart(container: HTMLElement, editorGroupsContainer: IEditorGroupsContainer, instantiationService: IInstantiationService): IAuxiliaryTitlebarPart {
@@ -386,7 +407,7 @@ export class TitleService extends MultiWindowParts<TitlebarPart> implements ITit
const disposables = new DisposableStore();
const titlebarPart = instantiationService.createInstance(AuxiliaryTitlebarPart, titlebarPartContainer, editorGroupsContainer);
const titlebarPart = this.doCreateAuxiliaryTitlebarPart(titlebarPartContainer, editorGroupsContainer, instantiationService);
disposables.add(this.registerPart(titlebarPart));
disposables.add(Event.runAndSubscribe(titlebarPart.onDidChange, () => titlebarPartContainer.style.height = `${titlebarPart.height}px`));
@@ -397,6 +418,10 @@ export class TitleService extends MultiWindowParts<TitlebarPart> implements ITit
return titlebarPart;
}
protected doCreateAuxiliaryTitlebarPart(container: HTMLElement, editorGroupsContainer: IEditorGroupsContainer, instantiationService: IInstantiationService): TitlebarPart & IAuxiliaryTitlebarPart {
return instantiationService.createInstance(AuxiliaryTitlebarPart, container, editorGroupsContainer, this.mainPart);
}
//#endregion
//#region Service Implementation
+7 -36
View File
@@ -236,7 +236,7 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService {
private readonly partVisibility: IPartVisibilityState = {
sidebar: true,
auxiliaryBar: true,
auxiliaryBar: false,
editor: false,
panel: false,
chatBar: true
@@ -1059,12 +1059,7 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService {
}
this.partVisibility.sidebar = !hidden;
// Adjust CSS - for hiding, defer adding the class until animation
// completes so the part stays visible during the exit animation.
if (!hidden) {
this.mainContainer.classList.remove(LayoutClasses.SIDEBAR_HIDDEN);
}
this.mainContainer.classList.toggle(LayoutClasses.SIDEBAR_HIDDEN, hidden);
// Propagate to grid
this.workbenchGrid.setViewVisible(
@@ -1087,12 +1082,7 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService {
}
this.partVisibility.auxiliaryBar = !hidden;
// Adjust CSS - for hiding, defer adding the class until animation
// completes so the part stays visible during the exit animation.
if (!hidden) {
this.mainContainer.classList.remove(LayoutClasses.AUXILIARYBAR_HIDDEN);
}
this.mainContainer.classList.toggle(LayoutClasses.AUXILIARYBAR_HIDDEN, hidden);
// Propagate to grid
this.workbenchGrid.setViewVisible(
@@ -1115,15 +1105,8 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService {
}
this.partVisibility.editor = !hidden;
// Adjust CSS for main container
if (hidden) {
this.mainContainer.classList.add(LayoutClasses.MAIN_EDITOR_AREA_HIDDEN);
this.mainContainer.classList.remove(LayoutClasses.EDITOR_MODAL_VISIBLE);
} else {
this.mainContainer.classList.remove(LayoutClasses.MAIN_EDITOR_AREA_HIDDEN);
this.mainContainer.classList.add(LayoutClasses.EDITOR_MODAL_VISIBLE);
}
this.mainContainer.classList.toggle(LayoutClasses.MAIN_EDITOR_AREA_HIDDEN, hidden);
this.mainContainer.classList.toggle(LayoutClasses.EDITOR_MODAL_VISIBLE, !hidden);
// Show/hide modal
if (hidden) {
@@ -1144,13 +1127,7 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService {
}
this.partVisibility.panel = !hidden;
// Adjust CSS - for hiding, defer adding the class until animation
// completes because `.nopanel .part.panel { display: none !important }`
// would instantly hide the panel content mid-animation.
if (!hidden) {
this.mainContainer.classList.remove(LayoutClasses.PANEL_HIDDEN);
}
this.mainContainer.classList.toggle(LayoutClasses.PANEL_HIDDEN, hidden);
// Propagate to grid
this.workbenchGrid.setViewVisible(
@@ -1173,13 +1150,7 @@ export class Workbench extends Disposable implements IWorkbenchLayoutService {
}
this.partVisibility.chatBar = !hidden;
// Adjust CSS
if (hidden) {
this.mainContainer.classList.add(LayoutClasses.CHATBAR_HIDDEN);
} else {
this.mainContainer.classList.remove(LayoutClasses.CHATBAR_HIDDEN);
}
this.mainContainer.classList.toggle(LayoutClasses.CHATBAR_HIDDEN, hidden);
// Propagate to grid
this.workbenchGrid.setViewVisible(this.chatBarPartView, !hidden);
@@ -39,7 +39,7 @@ import { ILabelService } from '../../../../platform/label/common/label.js';
import { parseAllHookFiles } from '../../../../workbench/contrib/chat/browser/promptSyntax/hookUtils.js';
import { OS } from '../../../../base/common/platform.js';
import { IRemoteAgentService } from '../../../../workbench/services/remote/common/remoteAgentService.js';
import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js';
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { Action, Separator } from '../../../../base/common/actions.js';
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
@@ -340,7 +340,7 @@ export class AICustomizationListWidget extends Disposable {
@IPathService private readonly pathService: IPathService,
@ILabelService private readonly labelService: ILabelService,
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
@ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService,
@ISessionsManagementService private readonly activeSessionService: ISessionsManagementService,
@ILogService private readonly logService: ILogService,
@IClipboardService private readonly clipboardService: IClipboardService,
@ISCMService private readonly scmService: ISCMService,
@@ -5,7 +5,7 @@
import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
import { URI } from '../../../../base/common/uri.js';
import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js';
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
import { localize } from '../../../../nls.js';
import { MenuId } from '../../../../platform/actions/common/actions.js';
@@ -96,7 +96,7 @@ export const SIDEBAR_MIN_WIDTH = 150;
export const SIDEBAR_MAX_WIDTH = 350;
export const CONTENT_MIN_WIDTH = 400;
export function getActiveSessionRoot(activeSessionService: ISessionsWorkbenchService): URI | undefined {
export function getActiveSessionRoot(activeSessionService: ISessionsManagementService): URI | undefined {
const session = activeSessionService.getActiveSession();
return session?.worktree ?? session?.repository;
}
@@ -59,7 +59,7 @@ import { showConfigureHooksQuickPick } from '../../../../workbench/contrib/chat/
import { CustomizationCreatorService } from './customizationCreatorService.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js';
import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js';
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js';
import { IWorkingCopyService } from '../../../../workbench/services/workingCopy/common/workingCopyService.js';
@@ -176,7 +176,7 @@ export class AICustomizationManagementEditor extends EditorPane {
@IConfigurationService private readonly configurationService: IConfigurationService,
@ILayoutService private readonly layoutService: ILayoutService,
@ICommandService private readonly commandService: ICommandService,
@ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService,
@ISessionsManagementService private readonly activeSessionService: ISessionsManagementService,
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
) {
@@ -27,7 +27,7 @@ import { AICustomizationManagementEditorInput } from './aiCustomizationManagemen
import { AICustomizationManagementEditor } from './aiCustomizationManagementEditor.js';
import { agentIcon, instructionsIcon, promptIcon, skillIcon } from '../../aiCustomizationTreeView/browser/aiCustomizationTreeViewIcons.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js';
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
const $ = DOM.$;
@@ -66,7 +66,7 @@ export class AICustomizationOverviewView extends ViewPane {
@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
@IPromptsService private readonly promptsService: IPromptsService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService,
@ISessionsManagementService private readonly activeSessionService: ISessionsManagementService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js';
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js';
import { IChatService } from '../../../../workbench/contrib/chat/common/chatService/chatService.js';
import { ChatModeKind } from '../../../../workbench/contrib/chat/common/constants.js';
@@ -30,7 +30,7 @@ export class CustomizationCreatorService {
@ICommandService private readonly commandService: ICommandService,
@IChatService private readonly chatService: IChatService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
@ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService,
@ISessionsManagementService private readonly activeSessionService: ISessionsManagementService,
@IPromptsService private readonly promptsService: IPromptsService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
) { }
@@ -35,7 +35,7 @@ import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';
import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js';
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
//#region Context Keys
@@ -487,7 +487,7 @@ export class AICustomizationViewPane extends ViewPane {
@IMenuService private readonly menuService: IMenuService,
@ILogService private readonly logService: ILogService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService,
@ISessionsManagementService private readonly activeSessionService: ISessionsManagementService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
@@ -13,7 +13,7 @@ import { Codicon } from '../../../../base/common/codicons.js';
import { MarkdownString } from '../../../../base/common/htmlContent.js';
import { Iterable } from '../../../../base/common/iterator.js';
import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
import { autorun, derived, observableFromEvent, observableValue } from '../../../../base/common/observable.js';
import { autorun, derived, derivedOpts, IObservable, IObservableWithChange, observableFromEvent, observableValue } from '../../../../base/common/observable.js';
import { basename, dirname } from '../../../../base/common/path.js';
import { isEqual } from '../../../../base/common/resources.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
@@ -44,7 +44,6 @@ import { IResourceLabel, ResourceLabels } from '../../../../workbench/browser/la
import { ViewPane, IViewPaneOptions, ViewAction } from '../../../../workbench/browser/parts/views/viewPane.js';
import { ViewPaneContainer } from '../../../../workbench/browser/parts/views/viewPaneContainer.js';
import { IViewDescriptorService } from '../../../../workbench/common/views.js';
import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js';
import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js';
import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js';
import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js';
@@ -56,6 +55,7 @@ import { IActivityService, NumberBadge } from '../../../../workbench/services/ac
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../../workbench/services/editor/common/editorService.js';
import { IExtensionService } from '../../../../workbench/services/extensions/common/extensions.js';
import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js';
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
const $ = dom.$;
@@ -94,6 +94,11 @@ interface IChangesFolderItem {
readonly name: string;
}
interface IActiveSession {
readonly resource: URI;
readonly sessionType: string;
}
type ChangesTreeElement = IChangesFileItem | IChangesFolderItem;
function isChangesFileItem(element: ChangesTreeElement): element is IChangesFileItem {
@@ -201,8 +206,14 @@ export class ChangesViewPane extends ViewPane {
this.storageService.store('changesView.viewMode', mode, StorageScope.WORKSPACE, StorageTarget.USER);
}
// Track the active session's editing session resource
private readonly activeSessionResource = observableValue<URI | undefined>(this, undefined);
// Track the active session used by this view
private readonly activeSession: IObservableWithChange<IActiveSession | undefined>;
private readonly activeSessionFileCountObs: IObservableWithChange<number>;
private readonly activeSessionHasChangesObs: IObservableWithChange<boolean>;
get activeSessionHasChanges(): IObservable<boolean> {
return this.activeSessionHasChangesObs;
}
// Badge for file count
private readonly badgeDisposable = this._register(new MutableDisposable());
@@ -220,9 +231,9 @@ export class ChangesViewPane extends ViewPane {
@IHoverService hoverService: IHoverService,
@IChatEditingService private readonly chatEditingService: IChatEditingService,
@IEditorService private readonly editorService: IEditorService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
@IActivityService private readonly activityService: IActivityService,
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
@ISessionsManagementService private readonly sessionManagementService: ISessionsManagementService,
@ILabelService private readonly labelService: ILabelService,
@IStorageService private readonly storageService: IStorageService,
) {
@@ -235,113 +246,85 @@ export class ChangesViewPane extends ViewPane {
this.viewModeContextKey = changesViewModeContextKey.bindTo(contextKeyService);
this.viewModeContextKey.set(initialMode);
// Track active session from sessions management service
this.activeSession = derivedOpts<IActiveSession | undefined>({
equalsFn: (a, b) => isEqual(a?.resource, b?.resource),
}, reader => {
const activeSession = this.sessionManagementService.activeSession.read(reader);
if (!activeSession?.resource) {
return undefined;
}
return {
resource: activeSession.resource,
sessionType: getChatSessionType(activeSession.resource),
};
}).recomputeInitiallyAndOnChange(this._store);
this.activeSessionFileCountObs = this.createActiveSessionFileCountObservable();
this.activeSessionHasChangesObs = this.activeSessionFileCountObs.map(fileCount => fileCount > 0).recomputeInitiallyAndOnChange(this._store);
// Setup badge tracking
this.registerBadgeTracking();
// Track active session from focused chat widgets
this.registerActiveSessionTracking();
// Set chatSessionType on the view's context key service so ViewTitle
// menu items can use it in their `when` clauses. Update reactively
// when the active session changes.
const viewSessionTypeKey = this.scopedContextKeyService.createKey<string>(ChatContextKeys.agentSessionType.key, '');
this._register(autorun(reader => {
const sessionResource = this.activeSessionResource.read(reader);
viewSessionTypeKey.set(sessionResource ? getChatSessionType(sessionResource) : '');
const activeSession = this.activeSession.read(reader);
viewSessionTypeKey.set(activeSession?.sessionType ?? '');
}));
}
private registerActiveSessionTracking(): void {
// Initialize with the last focused widget's session if available
const lastFocused = this.chatWidgetService.lastFocusedWidget;
if (lastFocused?.viewModel?.sessionResource) {
this.activeSessionResource.set(lastFocused.viewModel.sessionResource, undefined);
}
// Listen for new widgets and track their focus
this._register(this.chatWidgetService.onDidAddWidget(widget => {
this._register(widget.onDidFocus(() => {
if (widget.viewModel?.sessionResource) {
this.activeSessionResource.set(widget.viewModel.sessionResource, undefined);
}
}));
// Also track view model changes (when a widget loads a different session)
this._register(widget.onDidChangeViewModel(({ currentSessionResource }) => {
// Only update if this widget is focused
if (this.chatWidgetService.lastFocusedWidget === widget && currentSessionResource) {
this.activeSessionResource.set(currentSessionResource, undefined);
}
}));
}));
// Track focus changes on existing widgets
for (const widget of this.chatWidgetService.getAllWidgets()) {
this._register(widget.onDidFocus(() => {
if (widget.viewModel?.sessionResource) {
this.activeSessionResource.set(widget.viewModel.sessionResource, undefined);
}
}));
this._register(widget.onDidChangeViewModel(({ currentSessionResource }) => {
if (this.chatWidgetService.lastFocusedWidget === widget && currentSessionResource) {
this.activeSessionResource.set(currentSessionResource, undefined);
}
}));
}
}
private registerBadgeTracking(): void {
// Signal observable that triggers when sessions data changes
// Update badge when file count changes
this._register(autorun(reader => {
const fileCount = this.activeSessionFileCountObs.read(reader);
this.updateBadge(fileCount);
}));
}
private createActiveSessionFileCountObservable(): IObservableWithChange<number> {
const activeSessionResource = this.activeSession.map(a => a?.resource);
const sessionsChangedSignal = observableFromEvent(
this,
this.agentSessionsService.model.onDidChangeSessions,
() => ({}),
);
// Observable for session file changes from agentSessionsService (cloud/background sessions)
// Reactive to both activeSessionResource changes AND session data changes
const sessionFileChangesObs = derived(reader => {
const sessionResource = this.activeSessionResource.read(reader);
const sessionResource = activeSessionResource.read(reader);
sessionsChangedSignal.read(reader);
if (!sessionResource) {
return Iterable.empty();
}
const model = this.agentSessionsService.getSession(sessionResource);
return model?.changes instanceof Array ? model.changes : Iterable.empty();
});
// Create observable for the number of files changed in the active session
// Combines both editing session entries and session file changes (for cloud/background sessions)
const fileCountObs = derived(reader => {
const sessionResource = this.activeSessionResource.read(reader);
if (!sessionResource) {
return derived(reader => {
const activeSession = this.activeSession.read(reader);
if (!activeSession) {
return 0;
}
// Background chat sessions render the working set based on the session files, not the editing session
const isBackgroundSession = getChatSessionType(sessionResource) === AgentSessionProviders.Background;
const isBackgroundSession = activeSession.sessionType === AgentSessionProviders.Background;
// Count from editing session entries (skip for background sessions)
let editingSessionCount = 0;
if (!isBackgroundSession) {
const sessions = this.chatEditingService.editingSessionsObs.read(reader);
const session = sessions.find(candidate => isEqual(candidate.chatSessionResource, sessionResource));
const session = sessions.find(candidate => isEqual(candidate.chatSessionResource, activeSession.resource));
editingSessionCount = session ? session.entries.read(reader).length : 0;
}
// Count from session file changes (cloud/background sessions)
const sessionFiles = [...sessionFileChangesObs.read(reader)];
const sessionFilesCount = sessionFiles.length;
return editingSessionCount + sessionFilesCount;
});
// Update badge when file count changes
this._register(autorun(reader => {
const fileCount = fileCountObs.read(reader);
this.updateBadge(fileCount);
}));
}).recomputeInitiallyAndOnChange(this._store);
}
private updateBadge(fileCount: number): void {
@@ -404,25 +387,26 @@ export class ChangesViewPane extends ViewPane {
private onVisible(): void {
this.renderDisposables.clear();
const activeSessionResource = this.activeSession.map(a => a?.resource);
// Create observable for the active editing session
// Note: We must read editingSessionsObs to establish a reactive dependency,
// so that the view updates when a new editing session is added (e.g., cloud sessions)
const activeEditingSessionObs = derived(reader => {
const sessionResource = this.activeSessionResource.read(reader);
if (!sessionResource) {
const activeSession = this.activeSession.read(reader);
if (!activeSession) {
return undefined;
}
const sessions = this.chatEditingService.editingSessionsObs.read(reader);
return sessions.find(candidate => isEqual(candidate.chatSessionResource, sessionResource));
return sessions.find(candidate => isEqual(candidate.chatSessionResource, activeSession.resource));
});
// Create observable for edit session entries from the ACTIVE session only (local editing sessions)
const editSessionEntriesObs = derived(reader => {
const sessionResource = this.activeSessionResource.read(reader);
const activeSession = this.activeSession.read(reader);
// Background chat sessions render the working set based on the session files, not the editing session
if (sessionResource && getChatSessionType(sessionResource) === AgentSessionProviders.Background) {
if (activeSession?.sessionType === AgentSessionProviders.Background) {
return [];
}
@@ -462,9 +446,9 @@ export class ChangesViewPane extends ViewPane {
);
// Observable for session file changes from agentSessionsService (cloud/background sessions)
// Reactive to both activeSessionResource changes AND session data changes
// Reactive to both activeSession changes AND session data changes
const sessionFileChangesObs = derived(reader => {
const sessionResource = this.activeSessionResource.read(reader);
const sessionResource = activeSessionResource.read(reader);
sessionsChangedSignal.read(reader);
if (!sessionResource) {
return Iterable.empty();
@@ -530,8 +514,8 @@ export class ChangesViewPane extends ViewPane {
// `chatSessionType == copilotcli` (e.g. Create Pull Request) are shown
const chatSessionTypeKey = scopedContextKeyService.createKey<string>(ChatContextKeys.agentSessionType.key, '');
this.renderDisposables.add(autorun(reader => {
const sessionResource = this.activeSessionResource.read(reader);
chatSessionTypeKey.set(sessionResource ? getChatSessionType(sessionResource) : '');
const activeSession = this.activeSession.read(reader);
chatSessionTypeKey.set(activeSession?.sessionType ?? '');
}));
// Bind required context keys for the menu buttons
@@ -560,7 +544,7 @@ export class ChangesViewPane extends ViewPane {
this.renderDisposables.add(autorun(reader => {
const { isSessionMenu, added, removed } = topLevelStats.read(reader);
const sessionResource = this.activeSessionResource.read(reader);
const sessionResource = activeSessionResource.read(reader);
reader.store.add(scopedInstantiationService.createInstance(
MenuWorkbenchButtonBar,
this.actionsContainer!,
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Codicon } from '../../../../base/common/codicons.js';
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { localize, localize2 } from '../../../../nls.js';
import { Action2, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
@@ -14,15 +15,19 @@ import { Registry } from '../../../../platform/registry/common/platform.js';
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js';
import { isAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js';
import { ISessionsWorkbenchService, IsNewChatSessionContext } from '../../sessions/browser/sessionsWorkbenchService.js';
import { ITerminalService, ITerminalGroupService } from '../../../../workbench/contrib/terminal/browser/terminal.js';
import { ISessionsManagementService, IsNewChatSessionContext } from '../../sessions/browser/sessionsManagementService.js';
import { ITerminalService } from '../../../../workbench/contrib/terminal/browser/terminal.js';
import { TERMINAL_VIEW_ID } from '../../../../workbench/contrib/terminal/common/terminal.js';
import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js';
import { Menus } from '../../../browser/menus.js';
import { BranchChatSessionAction } from './branchChatSessionAction.js';
import { RunScriptContribution } from './runScriptAction.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { AgenticPromptsService } from './promptsService.js';
import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
import { ChatViewContainerId, ChatViewId } from '../../../../workbench/contrib/chat/browser/chat.js';
import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js';
import { NewChatViewPane, SessionsViewId } from './newChatViewPane.js';
import { ViewPaneContainer } from '../../../../workbench/browser/parts/views/viewPaneContainer.js';
import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
@@ -46,9 +51,9 @@ export class OpenSessionWorktreeInVSCodeAction extends Action2 {
override async run(accessor: ServicesAccessor,): Promise<void> {
const hostService = accessor.get(IHostService);
const agentSessionsService = accessor.get(ISessionsWorkbenchService);
const sessionsManagementService = accessor.get(ISessionsManagementService);
const activeSession = agentSessionsService.activeSession.get();
const activeSession = sessionsManagementService.activeSession.get();
if (!activeSession) {
return;
}
@@ -64,6 +69,33 @@ export class OpenSessionWorktreeInVSCodeAction extends Action2 {
}
registerAction2(OpenSessionWorktreeInVSCodeAction);
class NewChatInSessionsWindowAction extends Action2 {
constructor() {
super({
id: 'workbench.action.sessions.newChat',
title: localize2('chat.newEdits.label', "New Chat"),
category: CHAT_CATEGORY,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib + 2,
primary: KeyMod.CtrlCmd | KeyCode.KeyN,
secondary: [KeyMod.CtrlCmd | KeyCode.KeyL],
mac: {
primary: KeyMod.CtrlCmd | KeyCode.KeyN,
secondary: [KeyMod.WinCtrl | KeyCode.KeyL]
},
}
});
}
override run(accessor: ServicesAccessor): void {
const sessionsManagementService = accessor.get(ISessionsManagementService);
sessionsManagementService.openNewSession();
}
}
registerAction2(NewChatInSessionsWindowAction);
export class OpenSessionInTerminalAction extends Action2 {
constructor() {
@@ -81,10 +113,10 @@ export class OpenSessionInTerminalAction extends Action2 {
override async run(accessor: ServicesAccessor,): Promise<void> {
const terminalService = accessor.get(ITerminalService);
const terminalGroupService = accessor.get(ITerminalGroupService);
const agentSessionsService = accessor.get(ISessionsWorkbenchService);
const viewsService = accessor.get(IViewsService);
const sessionsManagementService = accessor.get(ISessionsManagementService);
const activeSession = agentSessionsService.activeSession.get();
const activeSession = sessionsManagementService.activeSession.get();
const repository = isAgentSession(activeSession) && activeSession.providerType !== AgentSessionProviders.Cloud
? activeSession.worktree
: undefined;
@@ -94,7 +126,7 @@ export class OpenSessionInTerminalAction extends Action2 {
terminalService.setActiveInstance(instance);
}
}
terminalGroupService.showPanel(true);
await viewsService.openView(TERMINAL_VIEW_ID, true);
}
}
@@ -12,7 +12,7 @@
height: 100%;
box-sizing: border-box;
overflow-x: hidden;
transition: justify-content 0.4s ease;
padding-bottom: 10%;
}
.chat-full-welcome.revealed {
@@ -29,32 +29,29 @@
overflow: visible;
}
/* Mascot */
.chat-full-welcome-mascot {
width: 80px;
height: 80px;
/* Watermark letterpress */
.chat-full-welcome-letterpress {
width: 100%;
max-width: 200px;
aspect-ratio: 1/1;
background-image: url('../../../../../workbench/browser/parts/editor/media/letterpress-dark.svg');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
background-repeat: no-repeat;
margin-top: 8px;
margin-bottom: 12px;
animation: chat-full-welcome-mascot-bounce 1s ease-in-out infinite;
transition: animation-duration 0.3s ease;
background-image: url('../../../../../workbench/browser/media/code-icon.svg');
margin-bottom: 20px;
}
.chat-full-welcome.revealed .chat-full-welcome-mascot {
animation-duration: 2s;
.vs .chat-full-welcome-letterpress {
background-image: url('../../../../../workbench/browser/parts/editor/media/letterpress-light.svg');
}
@keyframes chat-full-welcome-mascot-bounce {
0%, 100% {
transform: translateY(0);
}
.hc-light .chat-full-welcome-letterpress {
background-image: url('../../../../../workbench/browser/parts/editor/media/letterpress-hcLight.svg');
}
50% {
transform: translateY(-6px);
}
.hc-black .chat-full-welcome-letterpress {
background-image: url('../../../../../workbench/browser/parts/editor/media/letterpress-hcDark.svg');
}
/* Input slot */
@@ -71,18 +68,18 @@
animation: chat-full-welcome-fade-in 0.35s ease 0.15s both;
}
/* Option group pickers container (below the input) */
/* Option group pickers container (above the input) */
.chat-full-welcome-pickers-container {
display: none;
justify-content: center;
width: 100%;
max-width: 800px;
margin: 12px;
margin: 0 0 24px 0;
padding: 0;
box-sizing: border-box;
}
.chat-full-welcome.revealed .chat-full-welcome-pickers-container {
display: flex;
display: block;
animation: chat-full-welcome-fade-in 0.35s ease 0.1s both;
}
@@ -103,6 +100,39 @@
margin-bottom: 0;
}
/* Local mode picker (Workspace / Worktree) below input */
.chat-full-welcome-local-mode {
width: 100%;
max-width: 800px;
margin-top: 8px;
box-sizing: border-box;
display: none;
flex-direction: row;
align-items: center;
min-height: 28px;
}
.chat-full-welcome.revealed .chat-full-welcome-local-mode {
display: flex;
}
.sessions-chat-local-mode-left {
display: flex;
align-items: center;
min-width: 0;
}
.sessions-chat-local-mode-spacer {
flex: 1;
}
.sessions-chat-local-mode-right {
display: flex;
align-items: center;
gap: 2px;
min-width: 0;
}
/* Ensure the input editor fits properly */
.chat-full-welcome-inputSlot .interactive-input-part {
margin: 0;
@@ -128,7 +158,7 @@
background-color: var(--vscode-input-background) !important;
}
/* Pickers row - flat horizontal bar below the input */
/* Pickers row - two equal halves */
.chat-full-welcome-pickers {
display: flex;
flex-direction: row;
@@ -136,56 +166,76 @@
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 0 2px;
padding: 0;
}
.chat-full-welcome-pickers:empty {
display: none;
}
/* Left group (target dropdown + left-side extension pickers) */
.sessions-chat-pickers-left {
display: flex;
align-items: center;
gap: 2px;
min-width: 0;
}
/* Spacer between left and right groups */
.sessions-chat-pickers-spacer {
/* Left half: target switcher, right-justified */
.sessions-chat-pickers-left-half {
flex: 1;
}
/* Right group (repo/folder pickers) */
.sessions-chat-extension-pickers-right {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 2px;
min-width: 0;
}
.sessions-chat-extension-pickers-right:empty {
display: none;
}
/* Left extension pickers container */
.sessions-chat-extension-pickers-left {
/* Right half: pickers, left-justified */
.sessions-chat-pickers-right-half {
flex: 1;
display: flex;
justify-content: flex-start;
align-items: center;
gap: 2px;
min-width: 0;
}
.sessions-chat-extension-pickers-left:empty {
/* Separator between switcher and folder picker */
.sessions-chat-pickers-left-separator {
width: 1px;
height: 22px;
background-color: var(--vscode-editorWidget-border, var(--vscode-contrastBorder));
margin: 0 12px;
flex-shrink: 0;
display: none;
}
/* Target switcher radio buttons - bigger and fancier */
.sessions-chat-dropdown-wrapper .monaco-custom-radio > .monaco-button {
font-size: 13px;
line-height: 1.4em;
padding: 4px 14px;
}
.sessions-chat-dropdown-wrapper .monaco-custom-radio > .monaco-button:first-child {
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
}
.sessions-chat-dropdown-wrapper .monaco-custom-radio > .monaco-button:last-child {
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
.sessions-chat-dropdown-wrapper .monaco-custom-radio > .monaco-button:focus {
outline: none;
}
/* Folder label next to the picker */
.sessions-chat-folder-label {
font-size: 13px;
color: var(--vscode-descriptionForeground);
white-space: nowrap;
margin-right: 6px;
}
/* Target dropdown button */
.sessions-chat-dropdown-button {
display: flex;
align-items: center;
height: 16px;
padding: 3px 0 3px 6px;
padding: 3px 3px 3px 6px;
cursor: pointer;
font-size: 13px;
color: var(--vscode-descriptionForeground);
@@ -231,7 +281,7 @@
display: flex;
align-items: center;
height: 16px;
padding: 3px 0 3px 6px;
padding: 3px 3px 3px 6px;
background-color: transparent;
border: none;
color: var(--vscode-descriptionForeground);
@@ -7,7 +7,8 @@ import './media/chatWidget.css';
import './media/chatWelcomePart.css';
import * as dom from '../../../../base/browser/dom.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { toAction } from '../../../../base/common/actions.js';
import { Separator, toAction } from '../../../../base/common/actions.js';
import { Radio } from '../../../../base/browser/ui/radio/radio.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { KeyCode } from '../../../../base/common/keyCodes.js';
import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
@@ -25,17 +26,14 @@ import { IInstantiationService } from '../../../../platform/instantiation/common
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';
import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
import { isEqual } from '../../../../base/common/resources.js';
import { asCSSUrl } from '../../../../base/browser/cssValue.js';
import { FileAccess } from '../../../../base/common/network.js';
import { basename, isEqual } from '../../../../base/common/resources.js';
import { localize } from '../../../../nls.js';
import { AgentSessionProviders, getAgentSessionProviderIcon } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js';
import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js';
import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js';
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
import { ChatSessionPosition, getResourceForNewChatSession } from '../../../../workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.js';
import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from '../../../../workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.js';
import { SearchableOptionPickerActionItem } from '../../../../workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.js';
@@ -43,10 +41,15 @@ import { ChatAgentLocation, ChatModeKind } from '../../../../workbench/contrib/c
import { IChatSendRequestOptions } from '../../../../workbench/contrib/chat/common/chatService/chatService.js';
import { IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';
import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js';
import { IModelPickerDelegate, ModelPickerActionItem } from '../../../../workbench/contrib/chat/browser/widget/input/modelPickerActionItem.js';
import { IModelPickerDelegate } from '../../../../workbench/contrib/chat/browser/widget/input/modelPickerActionItem.js';
import { EnhancedModelPickerActionItem } from '../../../../workbench/contrib/chat/browser/widget/input/modelPickerActionItem2.js';
import { IChatInputPickerOptions } from '../../../../workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.js';
import { WorkspaceFolderCountContext } from '../../../../workbench/common/contextkeys.js';
import { IViewDescriptorService } from '../../../../workbench/common/views.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { IWorkspacesService, isRecentFolder } from '../../../../platform/workspaces/common/workspaces.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js';
import { ContextMenuController } from '../../../../editor/contrib/contextmenu/browser/contextmenu.js';
import { getSimpleEditorOptions } from '../../../../workbench/contrib/codeEditor/browser/simpleEditorOptions.js';
@@ -104,6 +107,20 @@ class TargetConfig extends Disposable implements ITargetConfig {
this._onDidChangeSelectedTarget.fire(target);
}
}
setAllowedTargets(targets: AgentSessionProviders[]): void {
const newSet = new Set(targets);
this._allowedTargets.set(newSet, undefined);
this._onDidChangeAllowedTargets.fire(newSet);
// If the currently selected target is no longer allowed, switch to the first allowed target
const current = this._selectedTarget.get();
if (current && !newSet.has(current)) {
const fallback = newSet.values().next().value;
this._selectedTarget.set(fallback, undefined);
this._onDidChangeSelectedTarget.fire(fallback);
}
}
}
// #endregion
@@ -119,6 +136,7 @@ export interface INewChatSendRequestData {
readonly query: string;
readonly sendOptions: IChatSendRequestOptions;
readonly selectedOptions: ReadonlyMap<string, IChatSessionProviderOptionItem>;
readonly folderUri?: URI;
}
/**
@@ -139,7 +157,7 @@ export interface INewChatWidgetOptions {
*/
class NewChatWidget extends Disposable {
private readonly _targetConfig: ITargetConfig;
private readonly _targetConfig: TargetConfig;
private readonly _options: INewChatWidgetOptions;
// Input
@@ -155,6 +173,11 @@ class NewChatWidget extends Disposable {
private _extensionPickersLeftContainer: HTMLElement | undefined;
private _extensionPickersRightContainer: HTMLElement | undefined;
private _inputSlot: HTMLElement | undefined;
private _localModeContainer: HTMLElement | undefined;
private _localModeDropdownContainer: HTMLElement | undefined;
private _localModePickersContainer: HTMLElement | undefined;
private _localMode: 'workspace' | 'worktree' = 'worktree';
private _selectedFolderUri: URI | undefined;
private readonly _pickerWidgets = new Map<string, ChatSessionPickerActionItem | SearchableOptionPickerActionItem>();
private readonly _pickerWidgetDisposables = this._register(new DisposableStore());
private readonly _optionEmitters = new Map<string, Emitter<IChatSessionProviderOptionItem>>();
@@ -171,19 +194,29 @@ class NewChatWidget extends Disposable {
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@IProductService private readonly productService: IProductService,
@ILogService private readonly logService: ILogService,
@IHoverService _hoverService: IHoverService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@IWorkspacesService private readonly workspacesService: IWorkspacesService,
@IStorageService private readonly storageService: IStorageService,
) {
super();
this._targetConfig = this._register(new TargetConfig(options.targetConfig));
this._options = options;
// Restore last picked folder
const lastFolder = this.storageService.get('agentSessions.lastPickedFolder', StorageScope.PROFILE);
if (lastFolder) {
try { this._selectedFolderUri = URI.parse(lastFolder); } catch { /* ignore */ }
}
// When target changes, regenerate pending resource
this._register(this._targetConfig.onDidChangeSelectedTarget(() => {
this._generatePendingSessionResource();
this._updateTargetDropdown();
this._notifyFolderSelection();
this._renderExtensionPickers(true);
this._renderLocalModePicker();
}));
this._register(this._targetConfig.onDidChangeAllowedTargets(() => {
@@ -196,6 +229,7 @@ class NewChatWidget extends Disposable {
// Listen for option group changes to re-render pickers
this._register(this.chatSessionsService.onDidChangeOptionGroups(() => {
this._notifyFolderSelection();
this._renderExtensionPickers();
}));
@@ -224,11 +258,12 @@ class NewChatWidget extends Disposable {
const wrapper = dom.append(container, dom.$('.sessions-chat-widget'));
const welcomeElement = dom.append(wrapper, dom.$('.chat-full-welcome'));
// Mascot
// Watermark letterpress
const header = dom.append(welcomeElement, dom.$('.chat-full-welcome-header'));
const quality = this.productService.quality ?? 'stable';
const mascot = dom.append(header, dom.$('.chat-full-welcome-mascot'));
mascot.style.backgroundImage = asCSSUrl(FileAccess.asBrowserUri(`vs/sessions/contrib/chat/browser/media/code-icon-agent-sessions-${quality}.svg`));
dom.append(header, dom.$('.chat-full-welcome-letterpress'));
// Option group pickers (above the input)
this._pickersContainer = dom.append(welcomeElement, dom.$('.chat-full-welcome-pickers-container'));
// Input slot
this._inputSlot = dom.append(welcomeElement, dom.$('.chat-full-welcome-inputSlot'));
@@ -239,8 +274,11 @@ class NewChatWidget extends Disposable {
this._createToolbar(inputArea);
this._inputSlot.appendChild(inputArea);
// Option group pickers (below the input)
this._pickersContainer = dom.append(welcomeElement, dom.$('.chat-full-welcome-pickers-container'));
// Local mode picker (below the input, shown when Local is selected)
this._localModeContainer = dom.append(welcomeElement, dom.$('.chat-full-welcome-local-mode'));
this._localModeDropdownContainer = dom.append(this._localModeContainer, dom.$('.sessions-chat-local-mode-left'));
dom.append(this._localModeContainer, dom.$('.sessions-chat-local-mode-spacer'));
this._localModePickersContainer = dom.append(this._localModeContainer, dom.$('.sessions-chat-local-mode-right'));
// Render target buttons & extension pickers
this._renderOptionGroupPickers();
@@ -251,12 +289,23 @@ class NewChatWidget extends Disposable {
// Generate pending resource for option changes
this._generatePendingSessionResource();
// Render local mode picker
this._renderLocalModePicker();
// Reveal
welcomeElement.classList.add('revealed');
}
private _generatePendingSessionResource(): void {
private _getEffectiveTarget(): AgentSessionProviders | undefined {
const target = this._targetConfig.selectedTarget.get();
if (target === AgentSessionProviders.Local && this._localMode === 'worktree') {
return AgentSessionProviders.Background;
}
return target;
}
private _generatePendingSessionResource(): void {
const target = this._getEffectiveTarget();
if (!target || target === AgentSessionProviders.Local) {
this._pendingSessionResource = undefined;
return;
@@ -354,7 +403,7 @@ class NewChatWidget extends Disposable {
const action = { id: 'sessions.modelPicker', label: '', enabled: true, class: undefined, tooltip: '', run: () => { } };
const modelPicker = this.instantiationService.createInstance(
ModelPickerActionItem, action, undefined, delegate, pickerOptions,
EnhancedModelPickerActionItem, action, delegate, pickerOptions,
);
this._modelPickerDisposable.value = modelPicker;
modelPicker.render(container);
@@ -397,17 +446,15 @@ class NewChatWidget extends Disposable {
const pickersRow = dom.append(this._pickersContainer, dom.$('.chat-full-welcome-pickers'));
// Left group: target dropdown + non-repo extension pickers
const leftGroup = dom.append(pickersRow, dom.$('.sessions-chat-pickers-left'));
this._targetDropdownContainer = dom.append(leftGroup, dom.$('.sessions-chat-dropdown-wrapper'));
// Left half: target switcher (right-justified within its half)
const leftHalf = dom.append(pickersRow, dom.$('.sessions-chat-pickers-left-half'));
this._targetDropdownContainer = dom.append(leftHalf, dom.$('.sessions-chat-dropdown-wrapper'));
this._renderTargetDropdown(this._targetDropdownContainer);
this._extensionPickersLeftContainer = dom.append(leftGroup, dom.$('.sessions-chat-extension-pickers-left'));
// Spacer
dom.append(pickersRow, dom.$('.sessions-chat-pickers-spacer'));
// Right group: repo/folder pickers
this._extensionPickersRightContainer = dom.append(pickersRow, dom.$('.sessions-chat-extension-pickers-right'));
// Right half: separator + pickers (left-justified within its half)
const rightHalf = dom.append(pickersRow, dom.$('.sessions-chat-pickers-right-half'));
this._extensionPickersLeftContainer = dom.append(rightHalf, dom.$('.sessions-chat-pickers-left-separator'));
this._extensionPickersRightContainer = dom.append(rightHalf, dom.$('.sessions-chat-extension-pickers-right'));
this._renderExtensionPickers();
}
@@ -418,62 +465,146 @@ class NewChatWidget extends Disposable {
return;
}
const activeType = this._targetConfig.selectedTarget.get() ?? AgentSessionProviders.Background;
const icon = getAgentSessionProviderIcon(activeType);
const name = getAgentSessionProviderName(activeType);
const activeType = this._targetConfig.selectedTarget.get() ?? AgentSessionProviders.Local;
const targets = [AgentSessionProviders.Local, AgentSessionProviders.Cloud].filter(t => allowed.has(t));
const activeIndex = targets.indexOf(activeType);
const button = dom.append(container, dom.$('.sessions-chat-dropdown-button'));
const radio = new Radio({
items: targets.map(target => ({
text: getAgentSessionProviderName(target),
isActive: target === activeType,
})),
});
this._welcomeContentDisposables.add(radio);
container.appendChild(radio.domNode);
if (activeIndex >= 0) {
radio.setActiveItem(activeIndex);
}
this._welcomeContentDisposables.add(radio.onDidSelect(index => {
this._targetConfig.setSelectedTarget(targets[index]);
}));
}
// --- Local mode picker (Workspace / Worktree) ---
private readonly _localModeDisposables = this._register(new DisposableStore());
private _renderLocalModePicker(): void {
if (!this._localModeContainer || !this._localModeDropdownContainer || !this._localModePickersContainer) {
return;
}
this._localModeDisposables.clear();
dom.clearNode(this._localModeDropdownContainer);
dom.clearNode(this._localModePickersContainer);
const selectedTarget = this._targetConfig.selectedTarget.get();
if (selectedTarget !== AgentSessionProviders.Local) {
this._localModeContainer.style.visibility = 'hidden';
return;
}
this._localModeContainer.style.visibility = '';
// Dropdown button for Workspace / Worktree
const modeLabel = this._localMode === 'workspace'
? localize('localMode.workspace', "Workspace")
: localize('localMode.worktree', "Worktree");
const modeIcon = this._localMode === 'workspace' ? Codicon.folder : Codicon.worktree;
const button = dom.append(this._localModeDropdownContainer, dom.$('.sessions-chat-dropdown-button'));
button.tabIndex = 0;
button.role = 'button';
button.ariaHasPopup = 'true';
dom.append(button, renderIcon(icon));
dom.append(button, dom.$('span.sessions-chat-dropdown-label', undefined, name));
dom.append(button, renderIcon(modeIcon));
dom.append(button, dom.$('span.sessions-chat-dropdown-label', undefined, modeLabel));
dom.append(button, renderIcon(Codicon.chevronDown));
this._welcomeContentDisposables.add(dom.addDisposableListener(button, dom.EventType.CLICK, () => {
const currentAllowed = this._targetConfig.allowedTargets.get();
const currentActive = this._targetConfig.selectedTarget.get();
const actions = [...currentAllowed]
.filter(t => t !== AgentSessionProviders.Local)
.map(sessionType => {
const label = getAgentSessionProviderName(sessionType);
return toAction({
id: `target.${sessionType}`,
label,
checked: sessionType === currentActive,
run: () => this._targetConfig.setSelectedTarget(sessionType),
});
});
this._localModeDisposables.add(dom.addDisposableListener(button, dom.EventType.CLICK, () => {
const actions = [
toAction({
id: 'localMode.workspace',
label: localize('localMode.workspace', "Workspace"),
checked: this._localMode === 'workspace',
run: () => this._setLocalMode('workspace'),
}),
toAction({
id: 'localMode.worktree',
label: localize('localMode.worktree', "Worktree"),
checked: this._localMode === 'worktree',
run: () => this._setLocalMode('worktree'),
}),
];
this.contextMenuService.showContextMenu({
getAnchor: () => button,
getActions: () => actions,
});
}));
// Render pickers in the right side
this._renderLocalModePickers();
}
private _updateTargetDropdown(): void {
if (!this._targetDropdownContainer) {
private _setLocalMode(mode: 'workspace' | 'worktree'): void {
if (this._localMode !== mode) {
this._localMode = mode;
this._generatePendingSessionResource();
this._notifyFolderSelection();
this._renderLocalModePicker();
}
}
private _notifyFolderSelection(): void {
if (!this._pendingSessionResource) {
return;
}
dom.clearNode(this._targetDropdownContainer);
this._renderTargetDropdown(this._targetDropdownContainer);
const folderUri = this._selectedFolderUri ?? this.workspaceContextService.getWorkspace().folders[0]?.uri;
if (folderUri) {
this.chatSessionsService.notifySessionOptionsChange(
this._pendingSessionResource,
[{ optionId: 'repository', value: folderUri.fsPath }]
).catch((err) => this.logService.error('Failed to notify extension of folder selection:', err));
}
}
private _renderLocalModePickers(): void {
if (!this._localModePickersContainer) {
return;
}
dom.clearNode(this._localModePickersContainer);
if (this._localMode === 'worktree') {
// Worktree mode: render extension pickers for Background provider
this._renderExtensionPickersInContainer(this._localModePickersContainer, AgentSessionProviders.Background);
}
}
// --- Welcome: Extension option pickers ---
private _renderExtensionPickers(force?: boolean): void {
if (!this._extensionPickersLeftContainer || !this._extensionPickersRightContainer) {
if (!this._extensionPickersRightContainer) {
return;
}
const activeSessionType = this._targetConfig.selectedTarget.get();
const activeSessionType = this._getEffectiveTarget();
if (!activeSessionType) {
this._clearExtensionPickers();
return;
}
// For Local target, show folder picker in top row and handle bottom row
if (this._targetConfig.selectedTarget.get() === AgentSessionProviders.Local) {
this._clearExtensionPickers();
this._renderLocalFolderPickerInTopRow();
this._renderLocalModePicker();
return;
}
const optionGroups = this.chatSessionsService.getOptionGroupsForSessionType(activeSessionType);
if (!optionGroups || optionGroups.length === 0) {
this._clearExtensionPickers();
return;
}
@@ -503,7 +634,15 @@ class NewChatWidget extends Disposable {
return;
}
visibleGroups.sort((a, b) => (a.when ? 1 : 0) - (b.when ? 1 : 0));
visibleGroups.sort((a, b) => {
// Repo/folder pickers first, then others
const aRepo = isRepoOrFolderGroup(a) ? 0 : 1;
const bRepo = isRepoOrFolderGroup(b) ? 0 : 1;
if (aRepo !== bRepo) {
return aRepo - bRepo;
}
return (a.when ? 1 : 0) - (b.when ? 1 : 0);
});
if (!force && this._pickerWidgets.size === visibleGroups.length) {
const allMatch = visibleGroups.every(g => this._pickerWidgets.has(g.id));
@@ -514,6 +653,11 @@ class NewChatWidget extends Disposable {
this._clearExtensionPickers();
// Show the separator between target switcher and extension pickers
if (this._extensionPickersLeftContainer) {
this._extensionPickersLeftContainer.style.display = 'block';
}
for (const optionGroup of visibleGroups) {
const initialItem = this._getDefaultOptionForGroup(optionGroup);
const initialState = { group: optionGroup, item: initialItem };
@@ -556,15 +700,150 @@ class NewChatWidget extends Disposable {
this._pickerWidgetDisposables.add(widget);
this._pickerWidgets.set(optionGroup.id, widget);
// Repo/folder pickers go to the right; others go to the left
const isRightAligned = isRepoOrFolderGroup(optionGroup);
const targetContainer = isRightAligned ? this._extensionPickersRightContainer! : this._extensionPickersLeftContainer!;
// All pickers go to the right
const targetContainer = this._extensionPickersRightContainer!;
const slot = dom.append(targetContainer, dom.$('.sessions-chat-picker-slot'));
widget.render(slot);
}
}
private _renderLocalFolderPickerInTopRow(): void {
if (!this._extensionPickersRightContainer) {
return;
}
// Show the separator
if (this._extensionPickersLeftContainer) {
this._extensionPickersLeftContainer.style.display = 'block';
}
this._renderLocalFolderPickerInContainer(this._extensionPickersRightContainer, this._pickerWidgetDisposables);
}
private _renderLocalFolderPickerInContainer(container: HTMLElement, disposables: DisposableStore): void {
const currentFolderUri = this._selectedFolderUri ?? this.workspaceContextService.getWorkspace().folders[0]?.uri;
const folderName = currentFolderUri ? basename(currentFolderUri) : localize('pickFolder', "Pick Folder");
const slot = dom.append(container, dom.$('.sessions-chat-picker-slot'));
const button = dom.append(slot, dom.$('.sessions-chat-dropdown-button'));
button.tabIndex = 0;
button.role = 'button';
button.ariaHasPopup = 'true';
dom.append(button, dom.$('span.sessions-chat-dropdown-label', undefined, folderName));
dom.append(button, renderIcon(Codicon.chevronDown));
const switchFolder = async (folderUri: URI) => {
this._selectedFolderUri = folderUri;
this.storageService.store('agentSessions.lastPickedFolder', folderUri.toString(), StorageScope.PROFILE, StorageTarget.MACHINE);
this._notifyFolderSelection();
this._renderExtensionPickers(true);
};
disposables.add(dom.addDisposableListener(button, dom.EventType.CLICK, async () => {
const recentlyOpened = await this.workspacesService.getRecentlyOpened();
const recentFolders = recentlyOpened.workspaces
.filter(isRecentFolder)
.filter(r => !currentFolderUri || !isEqual(r.folderUri, currentFolderUri))
.slice(0, 10);
const actions = recentFolders.map(recent => toAction({
id: recent.folderUri.toString(),
label: recent.label || basename(recent.folderUri),
run: () => switchFolder(recent.folderUri),
}));
actions.push(new Separator());
actions.push(toAction({
id: 'browse',
label: localize('browseFolder', "Browse..."),
run: async () => {
const selected = await this.fileDialogService.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
title: localize('selectFolder', "Select Folder"),
});
if (selected?.[0]) {
await switchFolder(selected[0]);
}
},
}));
this.contextMenuService.showContextMenu({
getAnchor: () => button,
getActions: () => actions,
});
}));
}
private _renderExtensionPickersInContainer(container: HTMLElement, sessionType: AgentSessionProviders): void {
const optionGroups = this.chatSessionsService.getOptionGroupsForSessionType(sessionType);
if (!optionGroups || optionGroups.length === 0) {
return;
}
const visibleGroups: IChatSessionProviderOptionGroup[] = [];
for (const group of optionGroups) {
if (isModelOptionGroup(group)) {
continue;
}
if (group.id === 'repository') {
continue;
}
const hasItems = group.items.length > 0 || (group.commands || []).length > 0 || !!group.searchable;
const passesWhenClause = this._evaluateOptionGroupVisibility(group);
if (hasItems && passesWhenClause) {
visibleGroups.push(group);
}
}
for (const optionGroup of visibleGroups) {
const initialItem = this._getDefaultOptionForGroup(optionGroup);
const initialState = { group: optionGroup, item: initialItem };
if (initialItem) {
this._updateOptionContextKey(optionGroup.id, initialItem.id);
}
const emitter = this._getOrCreateOptionEmitter(optionGroup.id);
const itemDelegate: IChatSessionPickerDelegate = {
getCurrentOption: () => this._selectedOptions.get(optionGroup.id) ?? this._getDefaultOptionForGroup(optionGroup),
onDidChangeOption: emitter.event,
setOption: (option: IChatSessionProviderOptionItem) => {
this._selectedOptions.set(optionGroup.id, option);
this._updateOptionContextKey(optionGroup.id, option.id);
emitter.fire(option);
if (this._pendingSessionResource) {
this.chatSessionsService.notifySessionOptionsChange(
this._pendingSessionResource,
[{ optionId: optionGroup.id, value: option }]
).catch((err) => this.logService.error(`Failed to notify extension of ${optionGroup.id} change:`, err));
}
this._renderLocalModePickers();
},
getOptionGroup: () => {
const groups = this.chatSessionsService.getOptionGroupsForSessionType(sessionType);
return groups?.find((g: { id: string }) => g.id === optionGroup.id);
},
getSessionResource: () => this._pendingSessionResource,
};
const action = toAction({ id: optionGroup.id, label: optionGroup.name, run: () => { } });
const widget = this.instantiationService.createInstance(
optionGroup.searchable ? SearchableOptionPickerActionItem : ChatSessionPickerActionItem,
action, initialState, itemDelegate
);
this._localModeDisposables.add(widget);
const slot = dom.append(container, dom.$('.sessions-chat-picker-slot'));
widget.render(slot);
}
}
private _evaluateOptionGroupVisibility(optionGroup: { id: string; when?: string }): boolean {
if (!optionGroup.when) {
return true;
@@ -578,7 +857,7 @@ class NewChatWidget extends Disposable {
}
private _syncOptionsFromSession(sessionResource: URI): void {
const activeSessionType = this._targetConfig.selectedTarget.get();
const activeSessionType = this._getEffectiveTarget();
if (!activeSessionType) {
return;
}
@@ -640,7 +919,7 @@ class NewChatWidget extends Disposable {
this._pickerWidgets.clear();
this._optionEmitters.clear();
if (this._extensionPickersLeftContainer) {
dom.clearNode(this._extensionPickersLeftContainer);
this._extensionPickersLeftContainer.style.display = 'none';
}
if (this._extensionPickersRightContainer) {
dom.clearNode(this._extensionPickersRightContainer);
@@ -655,7 +934,7 @@ class NewChatWidget extends Disposable {
return;
}
const target = this._targetConfig.selectedTarget.get();
const target = this._getEffectiveTarget();
if (!target) {
this.logService.warn('ChatWelcomeWidget: No target selected, cannot create session');
return;
@@ -682,12 +961,15 @@ class NewChatWidget extends Disposable {
agentIdSilent: contribution?.type,
};
const folderUri = this._selectedFolderUri ?? this.workspaceContextService.getWorkspace().folders[0]?.uri;
this._options.onSendRequest?.({
resource,
target,
query,
sendOptions,
selectedOptions: new Map(this._selectedOptions),
folderUri,
});
}
@@ -704,6 +986,10 @@ class NewChatWidget extends Disposable {
focusInput(): void {
this._editor?.focus();
}
updateAllowedTargets(targets: AgentSessionProviders[]): void {
this._targetConfig.setAllowedTargets(targets);
}
}
// #endregion
@@ -730,7 +1016,8 @@ export class NewChatViewPane extends ViewPane {
@IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService,
@IHoverService hoverService: IHoverService,
@ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService,
@ISessionsManagementService private readonly activeSessionService: ISessionsManagementService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@ILogService private readonly logService: ILogService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
@@ -743,12 +1030,12 @@ export class NewChatViewPane extends ViewPane {
NewChatWidget,
{
targetConfig: {
allowedTargets: [AgentSessionProviders.Background, AgentSessionProviders.Cloud],
defaultTarget: AgentSessionProviders.Background,
allowedTargets: this.computeAllowedTargets(),
defaultTarget: AgentSessionProviders.Local,
},
onSendRequest: (data) => {
this.activeSessionService.openSessionAndSend(
data.resource, data.query, data.sendOptions, data.selectedOptions
this.activeSessionService.sendRequestForNewSession(
data.resource, data.query, data.sendOptions, data.selectedOptions, data.folderUri
).catch(e => this.logService.error('NewChatViewPane: Failed to open session and send request', e));
},
} satisfies INewChatWidgetOptions,
@@ -756,6 +1043,15 @@ export class NewChatViewPane extends ViewPane {
this._widget.render(container);
this._widget.focusInput();
this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => {
this._widget?.updateAllowedTargets(this.computeAllowedTargets());
}));
}
private computeAllowedTargets(): AgentSessionProviders[] {
const targets: AgentSessionProviders[] = [AgentSessionProviders.Local, AgentSessionProviders.Cloud];
return targets;
}
protected override layoutBody(height: number, width: number): void {
@@ -17,7 +17,7 @@ import { IWorkbenchEnvironmentService } from '../../../../workbench/services/env
import { IPathService } from '../../../../workbench/services/path/common/pathService.js';
import { ISearchService } from '../../../../workbench/services/search/common/search.js';
import { IUserDataProfileService } from '../../../../workbench/services/userDataProfile/common/userDataProfile.js';
import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js';
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
export class AgenticPromptsService extends PromptsService {
protected override createPromptFilesLocator(): PromptFilesLocator {
@@ -36,7 +36,7 @@ class AgenticPromptFilesLocator extends PromptFilesLocator {
@IUserDataProfileService userDataService: IUserDataProfileService,
@ILogService logService: ILogService,
@IPathService pathService: IPathService,
@ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService,
@ISessionsManagementService private readonly activeSessionService: ISessionsManagementService,
) {
super(
fileService,
@@ -13,7 +13,7 @@ import { IQuickInputService } from '../../../../platform/quickinput/common/quick
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js';
import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js';
import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js';
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
import { ITerminalService } from '../../../../workbench/contrib/terminal/browser/terminal.js';
import { Menus } from '../../../browser/menus.js';
@@ -54,7 +54,7 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr
constructor(
@IStorageService private readonly _storageService: IStorageService,
@ITerminalService private readonly _terminalService: ITerminalService,
@ISessionsWorkbenchService activeSessionService: ISessionsWorkbenchService,
@ISessionsManagementService activeSessionService: ISessionsManagementService,
@IQuickInputService private readonly _quickInputService: IQuickInputService,
) {
super();
@@ -8,7 +8,6 @@ import { Registry } from '../../../../platform/registry/common/platform.js';
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerDefaultConfigurations([{
overrides: {
'chat.agentsControl.clickBehavior': 'focus',
'chat.agentsControl.enabled': true,
'chat.agent.maxRequests': 1000,
'chat.restoreLastPanelSession': true,
@@ -13,8 +13,9 @@ import { ViewPaneContainer } from '../../../../workbench/browser/parts/views/vie
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { SessionsTitleBarContribution } from './sessionsTitleBarWidget.js';
import { SessionsAuxiliaryBarContribution } from './sessionsAuxiliaryBarContribution.js';
import { AgenticSessionsViewPane, SessionsViewId } from './sessionsViewPane.js';
import { SessionsWorkbenchService, ISessionsWorkbenchService } from './sessionsWorkbenchService.js';
import { SessionsManagementService, ISessionsManagementService } from './sessionsManagementService.js';
const agentSessionsViewIcon = registerIcon('chat-sessions-icon', Codicon.commentDiscussionSparkle, localize('agentSessionsViewIcon', 'Icon for Agent Sessions View'));
const AGENT_SESSIONS_VIEW_TITLE = localize2('agentSessions.view.label', "Sessions");
@@ -46,5 +47,6 @@ const agentSessionsViewDescriptor: IViewDescriptor = {
Registry.as<IViewsRegistry>(ViewContainerExtensions.ViewsRegistry).registerViews([agentSessionsViewDescriptor], agentSessionsViewContainer);
registerWorkbenchContribution2(SessionsTitleBarContribution.ID, SessionsTitleBarContribution, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(SessionsAuxiliaryBarContribution.ID, SessionsAuxiliaryBarContribution, WorkbenchPhase.AfterRestored);
registerSingleton(ISessionsWorkbenchService, SessionsWorkbenchService, InstantiationType.Delayed);
registerSingleton(ISessionsManagementService, SessionsManagementService, InstantiationType.Delayed);
@@ -0,0 +1,64 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { autorun } from '../../../../base/common/observable.js';
import { Disposable, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
import { IWorkbenchLayoutService, Parts } from '../../../../workbench/services/layout/browser/layoutService.js';
import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js';
import { CHANGES_VIEW_ID, ChangesViewPane } from '../../changesView/browser/changesView.js';
export class SessionsAuxiliaryBarContribution extends Disposable {
static readonly ID = 'workbench.contrib.sessionsAuxiliaryBarContribution';
private readonly activeChangesListener = this._register(new MutableDisposable<IDisposable>());
private activeChangesView: ChangesViewPane | null = null;
constructor(
@IViewsService private readonly viewsService: IViewsService,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
) {
super();
this.tryBindToChangesView();
this._register(this.viewsService.onDidChangeViewVisibility(e => {
if (e.id !== CHANGES_VIEW_ID) {
return;
}
this.tryBindToChangesView();
}));
}
private tryBindToChangesView(): void {
const changesView = this.viewsService.getViewWithId<ChangesViewPane>(CHANGES_VIEW_ID);
if (!changesView) {
this.activeChangesView = null;
this.activeChangesListener.clear();
return;
}
if (this.activeChangesView === changesView) {
return;
}
this.activeChangesView = changesView;
this.activeChangesListener.value = autorun(reader => {
const hasChanges = changesView.activeSessionHasChanges.read(reader);
this.syncAuxiliaryBarVisibility(hasChanges);
});
}
private syncAuxiliaryBarVisibility(hasChanges: boolean): void {
const shouldHideAuxiliaryBar = !hasChanges;
const isAuxiliaryBarVisible = this.layoutService.isVisible(Parts.AUXILIARYBAR_PART);
if (shouldHideAuxiliaryBar === !isAuxiliaryBarVisible) {
return;
}
this.layoutService.setPartHidden(shouldHideAuxiliaryBar, Parts.AUXILIARYBAR_PART);
}
}
@@ -12,12 +12,17 @@ import { IContextKey, IContextKeyService, RawContextKey } from '../../../../plat
import { ILogService } from '../../../../platform/log/common/log.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { ISessionOpenOptions, openSession as openSessionDefault } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.js';
import { ChatViewPaneTarget, IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js';
import { ChatViewId, ChatViewPaneTarget, IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js';
import { ChatViewPane } from '../../../../workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.js';
import { IChatSessionItem, IChatSessionProviderOptionItem, IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';
import { IChatService, IChatSendRequestOptions } from '../../../../workbench/contrib/chat/common/chatService/chatService.js';
import { ChatAgentLocation } from '../../../../workbench/contrib/chat/common/constants.js';
import { IAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js';
import { IAgentSession, isAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js';
import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js';
import { LocalChatSessionUri } from '../../../../workbench/contrib/chat/common/model/chatUri.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IWorkspaceEditingService } from '../../../../workbench/services/workspaces/common/workspaceEditing.js';
import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js';
export const IsNewChatSessionContext = new RawContextKey<boolean>('isNewChatSession', true);
@@ -43,7 +48,7 @@ export type IActiveSessionItem = (IChatSessionItem | IAgentSession) & {
readonly worktree: URI | undefined;
};
export interface ISessionsWorkbenchService {
export interface ISessionsManagementService {
readonly _serviceBrand: undefined;
/**
@@ -62,22 +67,22 @@ export interface ISessionsWorkbenchService {
*/
openSession(sessionResource: URI, openOptions?: ISessionOpenOptions): Promise<void>;
/**
* Open a new session, apply options, and send the initial request.
* This is the main entry point for the new-chat welcome widget.
*/
openSessionAndSend(sessionResource: URI, query: string, sendOptions: IChatSendRequestOptions, selectedOptions?: ReadonlyMap<string, IChatSessionProviderOptionItem>): Promise<void>;
/**
* Switch to the new-session view.
* No-op if the current session is already a new session.
*/
openNewSession(): void;
/**
* Open a new session, apply options, and send the initial request.
* This is the main entry point for the new-chat welcome widget.
*/
sendRequestForNewSession(sessionResource: URI, query: string, sendOptions: IChatSendRequestOptions, selectedOptions?: ReadonlyMap<string, IChatSessionProviderOptionItem>, folderUri?: URI): Promise<void>;
}
export const ISessionsWorkbenchService = createDecorator<ISessionsWorkbenchService>('sessionsWorkbenchService');
export const ISessionsManagementService = createDecorator<ISessionsManagementService>('sessionsManagementService');
export class SessionsWorkbenchService extends Disposable implements ISessionsWorkbenchService {
export class SessionsManagementService extends Disposable implements ISessionsManagementService {
declare readonly _serviceBrand: undefined;
@@ -96,6 +101,9 @@ export class SessionsWorkbenchService extends Disposable implements ISessionsWor
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ILogService private readonly logService: ILogService,
@IContextKeyService contextKeyService: IContextKeyService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService,
@IViewsService private readonly viewsService: IViewsService,
) {
super();
@@ -107,9 +115,7 @@ export class SessionsWorkbenchService extends Disposable implements ISessionsWor
this.lastSelectedSession = this.loadLastSelectedSession();
// Save on shutdown
this._register(this.storageService.onWillSaveState(() => {
this.saveLastSelectedSession();
}));
this._register(this.storageService.onWillSaveState(() => this.saveLastSelectedSession()));
// Update active session when session options change
this._register(this.chatSessionsService.onDidChangeSessionOptions(sessionResource => {
@@ -124,9 +130,7 @@ export class SessionsWorkbenchService extends Disposable implements ISessionsWor
}));
// Update active session when the agent sessions model changes (e.g., metadata updates with worktree/repository info)
this._register(this.agentSessionsService.model.onDidChangeSessions(() => {
this.refreshActiveSessionFromModel();
}));
this._register(this.agentSessionsService.model.onDidChangeSessions(() => this.refreshActiveSessionFromModel()));
}
private refreshActiveSessionFromModel(): void {
@@ -137,6 +141,12 @@ export class SessionsWorkbenchService extends Disposable implements ISessionsWor
const agentSession = this.agentSessionsService.model.getSession(currentActive.resource);
if (!agentSession) {
// Only switch sessions if the active session was a known agent session
// that got deleted. New session resources that aren't yet in the model
// should not trigger a switch.
if (isAgentSession(currentActive)) {
this.showNextSession();
}
return;
}
@@ -149,6 +159,19 @@ export class SessionsWorkbenchService extends Disposable implements ISessionsWor
this._activeSession.set(activeSessionItem, undefined);
}
private showNextSession(): void {
const sessions = this.agentSessionsService.model.sessions
.filter(s => !s.isArchived())
.sort((a, b) => (b.timing.lastRequestEnded ?? b.timing.created) - (a.timing.lastRequestEnded ?? a.timing.created));
if (sessions.length > 0) {
this.setActiveSession(sessions[0]);
this.instantiationService.invokeFunction(openSessionDefault, sessions[0]);
} else {
this.openNewSession();
}
}
private getRepositoryFromMetadata(metadata: { readonly [key: string]: unknown } | undefined): [URI | undefined, URI | undefined] {
if (!metadata) {
return [undefined, undefined];
@@ -189,44 +212,106 @@ export class SessionsWorkbenchService extends Disposable implements ISessionsWor
}
async openSession(sessionResource: URI, openOptions?: ISessionOpenOptions): Promise<void> {
const session = this.agentSessionsService.model.getSession(sessionResource);
if (session) {
this.isNewChatSessionContext.set(false);
this.setActiveSession(session);
await this.instantiationService.invokeFunction(openSessionDefault, session, openOptions);
this.isNewChatSessionContext.set(false);
const existingSession = this.agentSessionsService.model.getSession(sessionResource);
if (existingSession) {
await this.openExistingSession(existingSession, openOptions);
} else if (LocalChatSessionUri.isLocalSession(sessionResource)) {
await this.openLocalSession();
} else {
// For new sessions, load via the chat service first so the model
// is ready before the ChatViewPane renders it.
const modelRef = await this.chatService.loadSessionForResource(sessionResource, ChatAgentLocation.Chat, CancellationToken.None);
// Switch view only after the model is loaded so the ChatViewPane
// has content immediately when it becomes visible.
this.isNewChatSessionContext.set(false);
const chatWidget = await this.chatWidgetService.openSession(sessionResource, ChatViewPaneTarget);
if (!chatWidget?.viewModel) {
this.logService.warn(`[ActiveSessionService] Failed to open session: ${sessionResource.toString()}`);
modelRef?.dispose();
return;
}
const repository = this.getRepositoryFromSessionOption(sessionResource);
const activeSessionItem: IActiveSessionItem = {
resource: sessionResource,
label: chatWidget.viewModel.model.title || '',
timing: chatWidget.viewModel.model.timing,
repository,
worktree: undefined
};
this.logService.info(`[ActiveSessionService] Active session changed (new): ${sessionResource.toString()}, repository: ${repository?.toString() ?? 'none'}`);
this._activeSession.set(activeSessionItem, undefined);
await this.openNewRemoteSession(sessionResource);
}
}
async openSessionAndSend(sessionResource: URI, query: string, sendOptions: IChatSendRequestOptions, selectedOptions?: ReadonlyMap<string, IChatSessionProviderOptionItem>): Promise<void> {
// 1. Open the session in ChatViewPane - this transitions views,
// loads the model, and connects it to the ChatWidget so
// tool invocations work.
/**
* Open an existing agent session - set it as active and reveal it.
*/
private async openExistingSession(session: IAgentSession, openOptions?: ISessionOpenOptions): Promise<void> {
this.setActiveSession(session);
await this.instantiationService.invokeFunction(openSessionDefault, session, openOptions);
}
/**
* Open a fresh local chat session - show the ChatViewPane and clear the widget.
*/
private async openLocalSession(): Promise<void> {
const view = await this.viewsService.openView(ChatViewId) as ChatViewPane | undefined;
if (view) {
await view.widget.clear();
if (view.widget.viewModel) {
const folder = this.workspaceContextService.getWorkspace().folders[0];
const activeSessionItem: IActiveSessionItem = {
resource: view.widget.viewModel.sessionResource,
label: view.widget.viewModel.model.title || '',
timing: view.widget.viewModel.model.timing,
repository: folder?.uri,
worktree: undefined
};
this._activeSession.set(activeSessionItem, undefined);
}
}
}
/**
* Open a new remote session - load the model first, then show it in the ChatViewPane.
*/
private async openNewRemoteSession(sessionResource: URI): Promise<void> {
const modelRef = await this.chatService.loadSessionForResource(sessionResource, ChatAgentLocation.Chat, CancellationToken.None);
const chatWidget = await this.chatWidgetService.openSession(sessionResource, ChatViewPaneTarget);
if (!chatWidget?.viewModel) {
this.logService.warn(`[ActiveSessionService] Failed to open session: ${sessionResource.toString()}`);
modelRef?.dispose();
return;
}
const repository = this.getRepositoryFromSessionOption(sessionResource);
const activeSessionItem: IActiveSessionItem = {
resource: sessionResource,
label: chatWidget.viewModel.model.title || '',
timing: chatWidget.viewModel.model.timing,
repository,
worktree: undefined
};
this.logService.info(`[ActiveSessionService] Active session changed (new): ${sessionResource.toString()}, repository: ${repository?.toString() ?? 'none'}`);
this._activeSession.set(activeSessionItem, undefined);
}
async sendRequestForNewSession(sessionResource: URI, query: string, sendOptions: IChatSendRequestOptions, selectedOptions?: ReadonlyMap<string, IChatSessionProviderOptionItem>, folderUri?: URI): Promise<void> {
if (LocalChatSessionUri.isLocalSession(sessionResource)) {
await this.sendLocalSession(sessionResource, query, folderUri);
} else {
await this.sendCustomSession(sessionResource, query, sendOptions, selectedOptions);
}
}
/**
* Local sessions run directly through the ChatWidget.
* Set the workspace folder, open a fresh chat view, and submit via acceptInput.
*/
private async sendLocalSession(sessionResource: URI, query: string, folderUri?: URI): Promise<void> {
if (folderUri) {
await this.workspaceEditingService.updateFolders(0, this.workspaceContextService.getWorkspace().folders.length, [{ uri: folderUri }]);
}
await this.openSession(sessionResource);
// 2. Apply selected options to the contributed session
const widget = this.chatWidgetService.lastFocusedWidget;
if (widget) {
widget.setInput(query);
widget.acceptInput(query);
}
}
/**
* Custom sessions (worktree, cloud, etc.) go through the chat service.
* Apply selected options, send the request, then wait for the extension
* to create an agent session so it appears in the sidebar.
*/
private async sendCustomSession(sessionResource: URI, query: string, sendOptions: IChatSendRequestOptions, selectedOptions?: ReadonlyMap<string, IChatSessionProviderOptionItem>): Promise<void> {
// 1. Open the session - loads the model and shows the ChatViewPane
await this.openSession(sessionResource);
// 2. Apply selected options (repository, branch, etc.) to the contributed session
if (selectedOptions && selectedOptions.size > 0) {
const modelRef = this.chatService.getActiveSessionReference(sessionResource);
if (modelRef) {
@@ -245,22 +330,17 @@ export class SessionsWorkbenchService extends Disposable implements ISessionsWor
}
}
// 3. Snapshot existing session resources so we can detect the new one
// 3. Send the request
const existingResources = new Set(
this.agentSessionsService.model.sessions.map(s => s.resource.toString())
);
// 4. Send the request through the chat service - the model is now
// connected to the ChatWidget, so tools and rendering work.
const result = await this.chatService.sendRequest(sessionResource, query, sendOptions);
if (result.kind === 'rejected') {
this.logService.error(`[ActiveSessionService] sendRequest rejected: ${result.reason}`);
return;
}
// 5. After send, the extension creates an agent session. Wait for it
// and set it as the active session so the titlebar and sidebar
// reflect the new session.
// 4. Wait for the extension to create an agent session, then set it as active
let newSession = this.agentSessionsService.model.sessions.find(
s => !existingResources.has(s.resource.toString())
);
@@ -18,7 +18,7 @@ import { IWorkbenchContribution } from '../../../../workbench/common/contributio
import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';
import { URI } from '../../../../base/common/uri.js';
import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js';
import { ISessionsWorkbenchService } from './sessionsWorkbenchService.js';
import { ISessionsManagementService } from './sessionsManagementService.js';
import { FocusAgentSessionsAction } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsActions.js';
import { AgentSessionsPicker } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.js';
import { autorun } from '../../../../base/common/observable.js';
@@ -60,7 +60,7 @@ export class SessionsTitleBarWidget extends BaseActionViewItem {
options: IBaseActionViewItemOptions | undefined,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IHoverService private readonly hoverService: IHoverService,
@ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService,
@ISessionsManagementService private readonly activeSessionService: ISessionsManagementService,
@IChatService private readonly chatService: IChatService,
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
@ICommandService private readonly commandService: ICommandService,
@@ -344,7 +344,9 @@ export class SessionsTitleBarWidget extends BaseActionViewItem {
}
private _showSessionsPicker(): void {
const picker = this.instantiationService.createInstance(AgentSessionsPicker, undefined);
const picker = this.instantiationService.createInstance(AgentSessionsPicker, undefined, {
overrideSessionOpen: (session, openOptions) => this.activeSessionService.openSession(session.resource, openOptions)
});
picker.pickAgentSession();
}
@@ -23,7 +23,7 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js';
import { localize, localize2 } from '../../../../nls.js';
import { AgentSessionsControl } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsControl.js';
import { AgentSessionsFilter, AgentSessionsGrouping } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.js';
import { ISessionsWorkbenchService } from './sessionsWorkbenchService.js';
import { ISessionsManagementService } from './sessionsManagementService.js';
import { Action2, ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';
import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js';
@@ -73,7 +73,6 @@ const CUSTOMIZATIONS_COLLAPSED_KEY = 'agentSessions.customizationsCollapsed';
export class AgenticSessionsViewPane extends ViewPane {
private viewPaneContainer: HTMLElement | undefined;
private newSessionButtonContainer: HTMLElement | undefined;
private sessionsControlContainer: HTMLElement | undefined;
sessionsControl: AgentSessionsControl | undefined;
private aiCustomizationContainer: HTMLElement | undefined;
@@ -98,7 +97,7 @@ export class AgenticSessionsViewPane extends ViewPane {
@IMcpService private readonly mcpService: IMcpService,
@IStorageService private readonly storageService: IStorageService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService,
@ISessionsManagementService private readonly activeSessionService: ISessionsManagementService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
@@ -156,7 +155,7 @@ export class AgenticSessionsViewPane extends ViewPane {
const sessionsContent = DOM.append(sessionsSection, $('.agent-sessions-content'));
// New Session Button
const newSessionButtonContainer = this.newSessionButtonContainer = DOM.append(sessionsContent, $('.agent-sessions-new-button-container'));
const newSessionButtonContainer = DOM.append(sessionsContent, $('.agent-sessions-new-button-container'));
const newSessionButton = this._register(new Button(newSessionButtonContainer, { ...defaultButtonStyles, secondary: true }));
newSessionButton.label = localize('newSession', "New Session");
this._register(newSessionButton.onDidClick(() => this.activeSessionService.openNewSession()));
@@ -195,6 +194,8 @@ export class AgenticSessionsViewPane extends ViewPane {
if (!sessionsControl.reveal(activeSession.resource)) {
sessionsControl.clearFocus();
}
} else {
sessionsControl.clearFocus(); // clear selection when a new session is created
}
}));
@@ -402,14 +403,11 @@ export class AgenticSessionsViewPane extends ViewPane {
protected override layoutBody(height: number, width: number): void {
super.layoutBody(height, width);
if (!this.sessionsControl || !this.newSessionButtonContainer) {
if (!this.sessionsControl || !this.sessionsControlContainer) {
return;
}
const buttonHeight = this.newSessionButtonContainer.offsetHeight;
const customizationHeight = this.aiCustomizationContainer?.offsetHeight || 0;
const availableSessionsHeight = height - buttonHeight - customizationHeight;
this.sessionsControl.layout(availableSessionsHeight, width);
this.sessionsControl.layout(this.sessionsControlContainer.offsetHeight, width);
}
override focus(): void {
@@ -0,0 +1,135 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getZoomFactor } from '../../../base/browser/browser.js';
import { getWindow, getWindowId } from '../../../base/browser/dom.js';
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
import { IContextKeyService } from '../../../platform/contextkey/common/contextkey.js';
import { IContextMenuService } from '../../../platform/contextview/browser/contextView.js';
import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
import { INativeHostService } from '../../../platform/native/common/native.js';
import { IStorageService } from '../../../platform/storage/common/storage.js';
import { IThemeService } from '../../../platform/theme/common/themeService.js';
import { useWindowControlsOverlay } from '../../../platform/window/common/window.js';
import { IHostService } from '../../../workbench/services/host/browser/host.js';
import { IWorkbenchLayoutService, Parts } from '../../../workbench/services/layout/browser/layoutService.js';
import { IAuxiliaryTitlebarPart } from '../../../workbench/browser/parts/titlebar/titlebarPart.js';
import { IEditorGroupsContainer } from '../../../workbench/services/editor/common/editorGroupsService.js';
import { CodeWindow, mainWindow } from '../../../base/browser/window.js';
import { TitlebarPart, TitleService } from '../../browser/parts/titlebarPart.js';
export class NativeTitlebarPart extends TitlebarPart {
private cachedWindowControlStyles: { bgColor: string; fgColor: string } | undefined;
private cachedWindowControlHeight: number | undefined;
constructor(
id: string,
targetWindow: CodeWindow,
@IContextMenuService contextMenuService: IContextMenuService,
@IConfigurationService configurationService: IConfigurationService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@IContextKeyService contextKeyService: IContextKeyService,
@IHostService hostService: IHostService,
@INativeHostService private readonly nativeHostService: INativeHostService,
) {
super(id, targetWindow, contextMenuService, configurationService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService);
}
override updateStyles(): void {
super.updateStyles();
if (this.element) {
if (useWindowControlsOverlay(this.configurationService)) {
if (
!this.cachedWindowControlStyles ||
this.cachedWindowControlStyles.bgColor !== this.element.style.backgroundColor ||
this.cachedWindowControlStyles.fgColor !== this.element.style.color
) {
this.nativeHostService.updateWindowControls({
targetWindowId: getWindowId(getWindow(this.element)),
backgroundColor: this.element.style.backgroundColor,
foregroundColor: this.element.style.color
});
}
}
}
}
override layout(width: number, height: number): void {
super.layout(width, height);
if (useWindowControlsOverlay(this.configurationService)) {
const newHeight = Math.round(height * getZoomFactor(getWindow(this.element)));
if (newHeight !== this.cachedWindowControlHeight) {
this.cachedWindowControlHeight = newHeight;
this.nativeHostService.updateWindowControls({
targetWindowId: getWindowId(getWindow(this.element)),
height: newHeight
});
}
}
}
}
class MainNativeTitlebarPart extends NativeTitlebarPart {
constructor(
@IContextMenuService contextMenuService: IContextMenuService,
@IConfigurationService configurationService: IConfigurationService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@IContextKeyService contextKeyService: IContextKeyService,
@IHostService hostService: IHostService,
@INativeHostService nativeHostService: INativeHostService,
) {
super(Parts.TITLEBAR_PART, mainWindow, contextMenuService, configurationService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, nativeHostService);
}
}
class AuxiliaryNativeTitlebarPart extends NativeTitlebarPart implements IAuxiliaryTitlebarPart {
private static COUNTER = 1;
get height() { return this.minimumHeight; }
constructor(
readonly container: HTMLElement,
editorGroupsContainer: IEditorGroupsContainer,
private readonly mainTitlebar: TitlebarPart,
@IContextMenuService contextMenuService: IContextMenuService,
@IConfigurationService configurationService: IConfigurationService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@IContextKeyService contextKeyService: IContextKeyService,
@IHostService hostService: IHostService,
@INativeHostService nativeHostService: INativeHostService,
) {
const id = AuxiliaryNativeTitlebarPart.COUNTER++;
super(`workbench.parts.auxiliaryTitle.${id}`, getWindow(container), contextMenuService, configurationService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, nativeHostService);
}
override get preventZoom(): boolean {
return getZoomFactor(getWindow(this.element)) < 1 || !this.mainTitlebar.hasZoomableElements;
}
}
export class NativeTitleService extends TitleService {
protected override createMainTitlebarPart(): MainNativeTitlebarPart {
return this.instantiationService.createInstance(MainNativeTitlebarPart);
}
protected override doCreateAuxiliaryTitlebarPart(container: HTMLElement, editorGroupsContainer: IEditorGroupsContainer, instantiationService: IInstantiationService): AuxiliaryNativeTitlebarPart {
return instantiationService.createInstance(AuxiliaryNativeTitlebarPart, container, editorGroupsContainer, this.mainPart);
}
}
@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { InstantiationType, registerSingleton } from '../../platform/instantiation/common/extensions.js';
import { ITitleService } from '../../workbench/services/title/browser/titleService.js';
import { NativeTitleService } from './parts/titlebarPart.js';
registerSingleton(ITitleService, NativeTitleService, InstantiationType.Eager);
+3 -3
View File
@@ -351,9 +351,9 @@ import '../workbench/contrib/surveys/browser/nps.contribution.js';
import '../workbench/contrib/surveys/browser/languageSurveys.contribution.js';
// Welcome
import '../workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.js';
import '../workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.contribution.js';
import '../workbench/contrib/welcomeWalkthrough/browser/walkThrough.contribution.js';
// import '../workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.js';
// import '../workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.contribution.js';
// import '../workbench/contrib/welcomeWalkthrough/browser/walkThrough.contribution.js';
import '../workbench/contrib/welcomeViews/common/viewsWelcome.contribution.js';
import '../workbench/contrib/welcomeViews/common/newFile.contribution.js';
+1
View File
@@ -9,6 +9,7 @@ import './sessions.common.main.js';
//#region --- workbench (agentic desktop main)
import './electron-browser/sessions.main.js';
import './electron-browser/titleService.js';
import '../workbench/electron-browser/desktop.contribution.js';
//#endregion
@@ -264,7 +264,6 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape
const options: vscode.LanguageModelToolInvocationStreamOptions<any> = {
rawInput: context.rawInput,
chatRequestId: context.chatRequestId,
chatSessionId: context.chatSessionId,
chatSessionResource: context.chatSessionResource,
chatInteractionId: context.chatInteractionId
};
@@ -151,7 +151,13 @@ export class ExtHostTreeViews extends Disposable implements ExtHostTreeViewsShap
dispose: async () => {
// Wait for the registration promise to finish before doing the dispose.
await registerPromise;
this._treeViews.delete(viewId);
// Only notify the main thread if this view was not replaced by a new registration.
// When an extension disposes a view and immediately re-registers it, the new
// registration may have already updated _treeViews before this async dispose runs.
if (this._treeViews.get(viewId) === treeView) {
this._treeViews.delete(viewId);
this._proxy.$disposeTree(viewId);
}
treeView.dispose();
}
};
@@ -1108,6 +1114,5 @@ class ExtHostTreeView<T> extends Disposable {
this._refreshCancellationSource.dispose();
this._clearAll();
this._proxy.$disposeTree(this._viewId);
}
}
@@ -499,6 +499,30 @@ suite('ExtHostTreeView', function () {
});
});
test('dispose and re-register tree view', async () => {
const disposeTreeSpy = sinon.spy(target, '$disposeTree');
const registerSpy = sinon.spy(target, '$registerTreeViewDataProvider');
// Create, dispose, and re-register a tree view with the same id
const treeView1 = testObject.createTreeView('reRegisterTreeProvider', { treeDataProvider: aNodeTreeDataProvider() }, extensionsDescription);
treeView1.dispose();
const treeView2 = testObject.createTreeView('reRegisterTreeProvider', { treeDataProvider: aNodeTreeDataProvider() }, extensionsDescription);
// Let all pending microtasks (the async dispose) settle
await new Promise<void>(r => setTimeout(r, 0));
// The new view should work — $getChildren should return results, not reject
const elements = await testObject.$getChildren('reRegisterTreeProvider');
assert.deepStrictEqual(unBatchChildren(elements)?.map(e => e.handle), ['0/0:a', '0/0:b']);
// $registerTreeViewDataProvider should have been called twice (once per createTreeView)
assert.strictEqual(registerSpy.callCount, 2);
// $disposeTree should NOT have been called — the old async dispose should detect it was replaced
assert.strictEqual(disposeTreeSpy.callCount, 0);
treeView2.dispose();
});
test('reveal will throw an error if getParent is not implemented', () => {
const treeView = testObject.createTreeView('treeDataProvider', { treeDataProvider: aNodeTreeDataProvider() }, extensionsDescription);
return treeView.reveal({ key: 'a' })
+2 -9
View File
@@ -16,7 +16,6 @@ import { getRemoteName } from '../../platform/remote/common/remoteHosts.js';
import { getVirtualWorkspaceScheme } from '../../platform/workspace/common/virtualWorkspace.js';
import { IWorkingCopyService } from '../services/workingCopy/common/workingCopyService.js';
import { isNative } from '../../base/common/platform.js';
import { IPaneCompositePartService } from '../services/panecomposite/browser/panecomposite.js';
import { WebFileSystemAccess } from '../../platform/files/browser/webFileSystemAccess.js';
import { IProductService } from '../../platform/product/common/productService.js';
import { getTitleBarStyle } from '../../platform/window/common/window.js';
@@ -75,7 +74,6 @@ export class WorkbenchContextKeysHandler extends Disposable {
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IEditorService private readonly editorService: IEditorService,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
) {
super();
@@ -180,6 +178,7 @@ export class WorkbenchContextKeysHandler extends Disposable {
// Sidebar
this.sideBarVisibleContext = SideBarVisibleContext.bindTo(this.contextKeyService);
this.sideBarVisibleContext.set(this.layoutService.isVisible(Parts.SIDEBAR_PART));
// Title Bar
this.titleAreaVisibleContext = TitleBarVisibleContext.bindTo(this.contextKeyService);
@@ -245,14 +244,12 @@ export class WorkbenchContextKeysHandler extends Disposable {
this._register(this.layoutService.onDidChangePanelAlignment(alignment => this.panelAlignmentContext.set(alignment)));
this._register(this.paneCompositeService.onDidPaneCompositeClose(() => this.updateSideBarContextKeys()));
this._register(this.paneCompositeService.onDidPaneCompositeOpen(() => this.updateSideBarContextKeys()));
this._register(this.layoutService.onDidChangePartVisibility(() => {
this.mainEditorAreaVisibleContext.set(this.layoutService.isVisible(Parts.EDITOR_PART, mainWindow));
this.panelVisibleContext.set(this.layoutService.isVisible(Parts.PANEL_PART));
this.panelMaximizedContext.set(this.layoutService.isPanelMaximized());
this.auxiliaryBarVisibleContext.set(this.layoutService.isVisible(Parts.AUXILIARYBAR_PART));
this.sideBarVisibleContext.set(this.layoutService.isVisible(Parts.SIDEBAR_PART));
this.updateTitleBarContextKeys();
}));
@@ -326,10 +323,6 @@ export class WorkbenchContextKeysHandler extends Disposable {
}
}
private updateSideBarContextKeys(): void {
this.sideBarVisibleContext.set(this.layoutService.isVisible(Parts.SIDEBAR_PART));
}
private updateTitleBarContextKeys(): void {
this.titleAreaVisibleContext.set(this.layoutService.isVisible(Parts.TITLEBAR_PART, mainWindow));
this.titleBarStyleContext.set(getTitleBarStyle(this.configurationService));
@@ -78,8 +78,8 @@ export class ModalEditorPart {
if (e.target === modalElement) {
EventHelper.stop(e, true);
// Guide focus back into the modal when clicking outside modal
editorPartContainer.focus();
// Close modal when clicking outside the dialog
editorPart.close();
}
}));
@@ -68,11 +68,6 @@ export interface IBrowserViewWorkbenchService {
* Clear all storage data for the current workspace browser session
*/
clearWorkspaceStorage(): Promise<void>;
/**
* Get the endpoint for connecting to a browser view's CDP proxy server
*/
getDebugWebSocketEndpoint(): Promise<string>;
}
@@ -52,6 +52,7 @@ export const CONTEXT_BROWSER_CAN_GO_FORWARD = new RawContextKey<boolean>('browse
export const CONTEXT_BROWSER_FOCUSED = new RawContextKey<boolean>('browserFocused', true, localize('browser.editorFocused', "Whether the browser editor is focused"));
export const CONTEXT_BROWSER_STORAGE_SCOPE = new RawContextKey<string>('browserStorageScope', '', localize('browser.storageScope', "The storage scope of the current browser view"));
export const CONTEXT_BROWSER_HAS_URL = new RawContextKey<boolean>('browserHasUrl', false, localize('browser.hasUrl', "Whether the browser has a URL loaded"));
export const CONTEXT_BROWSER_HAS_ERROR = new RawContextKey<boolean>('browserHasError', false, localize('browser.hasError', "Whether the browser has a load error"));
export const CONTEXT_BROWSER_DEVTOOLS_OPEN = new RawContextKey<boolean>('browserDevToolsOpen', false, localize('browser.devToolsOpen', "Whether developer tools are open for the current browser view"));
export const CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE = new RawContextKey<boolean>('browserElementSelectionActive', false, localize('browser.elementSelectionActive', "Whether element selection is currently active"));
@@ -188,6 +189,7 @@ export class BrowserEditor extends EditorPane {
private _canGoForwardContext!: IContextKey<boolean>;
private _storageScopeContext!: IContextKey<string>;
private _hasUrlContext!: IContextKey<boolean>;
private _hasErrorContext!: IContextKey<boolean>;
private _devToolsOpenContext!: IContextKey<boolean>;
private _elementSelectionActiveContext!: IContextKey<boolean>;
@@ -195,6 +197,7 @@ export class BrowserEditor extends EditorPane {
private readonly _inputDisposables = this._register(new DisposableStore());
private overlayManager: BrowserOverlayManager | undefined;
private _elementSelectionCts: CancellationTokenSource | undefined;
private _consoleSessionCts: CancellationTokenSource | undefined;
private _screenshotTimeout: ReturnType<typeof setTimeout> | undefined;
constructor(
@@ -226,6 +229,7 @@ export class BrowserEditor extends EditorPane {
this._canGoForwardContext = CONTEXT_BROWSER_CAN_GO_FORWARD.bindTo(contextKeyService);
this._storageScopeContext = CONTEXT_BROWSER_STORAGE_SCOPE.bindTo(contextKeyService);
this._hasUrlContext = CONTEXT_BROWSER_HAS_URL.bindTo(contextKeyService);
this._hasErrorContext = CONTEXT_BROWSER_HAS_ERROR.bindTo(contextKeyService);
this._devToolsOpenContext = CONTEXT_BROWSER_DEVTOOLS_OPEN.bindTo(contextKeyService);
this._elementSelectionActiveContext = CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE.bindTo(contextKeyService);
@@ -367,6 +371,12 @@ export class BrowserEditor extends EditorPane {
// Update navigation bar and context keys from model
this.updateNavigationState(navEvent);
if (navEvent.url) {
this.startConsoleSession();
} else {
this.stopConsoleSession();
}
}));
this._inputDisposables.add(this._model.onDidChangeLoadingState(() => {
@@ -519,6 +529,7 @@ export class BrowserEditor extends EditorPane {
}
const error: IBrowserViewLoadError | undefined = this._model.error;
this._hasErrorContext.set(!!error);
if (error) {
// Update error content
@@ -759,6 +770,66 @@ export class BrowserEditor extends EditorPane {
}
}
async addConsoleLogsToChat(): Promise<void> {
const resourceUri = this.input?.resource;
if (!resourceUri) {
return;
}
const locator: IBrowserTargetLocator = { browserViewId: BrowserViewUri.getId(resourceUri) };
try {
const logs = await this.browserElementsService.getConsoleLogs(locator);
if (!logs) {
return;
}
const toAttach: IChatRequestVariableEntry[] = [];
toAttach.push({
id: 'console-logs-' + Date.now(),
name: localize('consoleLogs', 'Console Logs'),
fullName: localize('consoleLogs', 'Console Logs'),
value: logs,
kind: 'element',
icon: ThemeIcon.fromId(Codicon.output.id),
});
const widget = await this.chatWidgetService.revealWidget() ?? this.chatWidgetService.lastFocusedWidget;
widget?.attachmentModel?.addContext(...toAttach);
} catch (error) {
this.logService.error('BrowserEditor.addConsoleLogsToChat: Failed to get console logs', error);
}
}
private startConsoleSession(): void {
// don't restart if already running
if (this._consoleSessionCts) {
return;
}
const resourceUri = this.input?.resource;
if (!resourceUri || !this._model?.url) {
return;
}
const cts = new CancellationTokenSource();
this._consoleSessionCts = cts;
const locator: IBrowserTargetLocator = { browserViewId: BrowserViewUri.getId(resourceUri) };
this.browserElementsService.startConsoleSession(cts.token, locator).catch(error => {
if (!cts.token.isCancellationRequested) {
this.logService.error('BrowserEditor: Failed to start console session', error);
}
});
}
private stopConsoleSession(): void {
if (this._consoleSessionCts) {
this._consoleSessionCts.dispose(true);
this._consoleSessionCts = undefined;
}
}
/**
* Update navigation state and context keys
*/
@@ -907,6 +978,9 @@ export class BrowserEditor extends EditorPane {
this._elementSelectionCts = undefined;
}
// Cancel any active console session
this.stopConsoleSession();
// Cancel any scheduled screenshots
this.cancelScheduledScreenshot();
@@ -920,6 +994,7 @@ export class BrowserEditor extends EditorPane {
this._canGoBackContext.reset();
this._canGoForwardContext.reset();
this._hasUrlContext.reset();
this._hasErrorContext.reset();
this._storageScopeContext.reset();
this._devToolsOpenContext.reset();
this._elementSelectionActiveContext.reset();
@@ -11,7 +11,7 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind
import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js';
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { BrowserEditor, CONTEXT_BROWSER_CAN_GO_BACK, CONTEXT_BROWSER_CAN_GO_FORWARD, CONTEXT_BROWSER_DEVTOOLS_OPEN, CONTEXT_BROWSER_FOCUSED, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_STORAGE_SCOPE, CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE, CONTEXT_BROWSER_FIND_WIDGET_FOCUSED, CONTEXT_BROWSER_FIND_WIDGET_VISIBLE } from './browserEditor.js';
import { BrowserEditor, CONTEXT_BROWSER_CAN_GO_BACK, CONTEXT_BROWSER_CAN_GO_FORWARD, CONTEXT_BROWSER_DEVTOOLS_OPEN, CONTEXT_BROWSER_FOCUSED, CONTEXT_BROWSER_HAS_ERROR, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_STORAGE_SCOPE, CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE, CONTEXT_BROWSER_FIND_WIDGET_FOCUSED, CONTEXT_BROWSER_FIND_WIDGET_VISIBLE } from './browserEditor.js';
import { BrowserViewUri } from '../../../../platform/browserView/common/browserViewUri.js';
import { IBrowserViewWorkbenchService } from '../common/browserView.js';
import { BrowserViewStorageScope } from '../../../../platform/browserView/common/browserView.js';
@@ -55,7 +55,12 @@ class OpenIntegratedBrowserAction extends Action2 {
logBrowserOpen(telemetryService, options.url ? 'commandWithUrl' : 'commandWithoutUrl');
await editorService.openEditor({ resource }, group);
const editorPane = await editorService.openEditor({ resource }, group);
// Lock the group when opening to the side
if (options.openToSide && editorPane?.group) {
editorPane.group.lock(true);
}
}
}
@@ -223,7 +228,7 @@ class AddElementToChatAction extends Action2 {
category: BrowserCategory,
icon: Codicon.inspect,
f1: true,
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, enabled),
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate(), enabled),
toggled: CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE,
menu: {
id: MenuId.BrowserActionsToolbar,
@@ -249,6 +254,34 @@ class AddElementToChatAction extends Action2 {
}
}
class AddConsoleLogsToChatAction extends Action2 {
static readonly ID = 'workbench.action.browser.addConsoleLogsToChat';
constructor() {
const enabled = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('config.chat.sendElementsToChat.enabled', true));
super({
id: AddConsoleLogsToChatAction.ID,
title: localize2('browser.addConsoleLogsToChatAction', 'Add Console Logs to Chat'),
category: BrowserCategory,
icon: Codicon.output,
f1: true,
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate(), enabled),
menu: {
id: MenuId.BrowserActionsToolbar,
group: 'actions',
order: 2,
when: enabled
}
});
}
async run(accessor: ServicesAccessor, browserEditor = accessor.get(IEditorService).activeEditorPane): Promise<void> {
if (browserEditor instanceof BrowserEditor) {
await browserEditor.addConsoleLogsToChat();
}
}
}
class ToggleDevToolsAction extends Action2 {
static readonly ID = 'workbench.action.browser.toggleDevTools';
@@ -259,12 +292,12 @@ class ToggleDevToolsAction extends Action2 {
category: BrowserCategory,
icon: Codicon.terminal,
f1: true,
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL),
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate()),
toggled: ContextKeyExpr.equals(CONTEXT_BROWSER_DEVTOOLS_OPEN.key, true),
menu: {
id: MenuId.BrowserActionsToolbar,
group: 'actions',
order: 2,
order: 3,
},
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
@@ -290,7 +323,8 @@ class OpenInExternalBrowserAction extends Action2 {
category: BrowserCategory,
icon: Codicon.linkExternal,
f1: true,
precondition: BROWSER_EDITOR_ACTIVE,
// Note: We do allow opening in an external browser even if there is an error page shown
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL),
menu: {
id: MenuId.BrowserActionsToolbar,
group: ActionGroupPage,
@@ -422,7 +456,7 @@ class ShowBrowserFindAction extends Action2 {
title: localize2('browser.showFindAction', 'Find in Page'),
category: BrowserCategory,
f1: true,
precondition: BROWSER_EDITOR_ACTIVE,
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, CONTEXT_BROWSER_HAS_ERROR.negate()),
menu: {
id: MenuId.BrowserActionsToolbar,
group: ActionGroupPage,
@@ -537,6 +571,7 @@ registerAction2(GoForwardAction);
registerAction2(ReloadAction);
registerAction2(FocusUrlInputAction);
registerAction2(AddElementToChatAction);
registerAction2(AddConsoleLogsToChatAction);
registerAction2(ToggleDevToolsAction);
registerAction2(OpenInExternalBrowserAction);
registerAction2(ClearGlobalBrowserStorageAction);
@@ -54,8 +54,4 @@ export class BrowserViewWorkbenchService implements IBrowserViewWorkbenchService
const workspaceId = this.workspaceContextService.getWorkspace().id;
return this._browserViewService.clearWorkspaceStorage(workspaceId);
}
async getDebugWebSocketEndpoint() {
return this._browserViewService.getDebugWebSocketEndpoint();
}
}
@@ -605,14 +605,6 @@ export function registerChatActions() {
const chatVisible = viewsService.isViewVisible(ChatViewId);
const clickBehavior = configurationService.getValue<AgentsControlClickBehavior>(ChatConfiguration.AgentsControlClickBehavior);
switch (clickBehavior) {
case AgentsControlClickBehavior.Focus:
if (chatLocation === ViewContainerLocation.AuxiliaryBar) {
layoutService.setAuxiliaryBarMaximized(true);
} else {
this.updatePartVisibility(layoutService, chatLocation, true);
}
(await widgetService.revealWidget())?.focusInput();
break;
case AgentsControlClickBehavior.Cycle:
if (chatVisible) {
if (
@@ -129,6 +129,7 @@ class AttachFileToChatAction extends AttachResourceAction {
id: AttachFileToChatAction.ID,
title: localize2('workbench.action.chat.attachFile.label', "Add File to Chat"),
category: CHAT_CATEGORY,
icon: Codicon.attach,
precondition: ChatContextKeys.enabled,
f1: true,
menu: [{
@@ -173,9 +174,14 @@ class AttachFileToChatAction extends AttachResourceAction {
)
)
}, {
id: MenuId.ChatEditorInlineGutter,
group: '2_chat',
order: 2,
id: MenuId.InlineChatEditorAffordance,
group: '0_chat',
order: 3,
when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection.negate())
}, {
id: MenuId.ChatEditorInlineMenu,
group: '0_chat',
order: 3,
when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection.negate())
}]
});
@@ -290,6 +296,7 @@ class AttachSelectionToChatAction extends Action2 {
id: AttachSelectionToChatAction.ID,
title: localize2('workbench.action.chat.attachSelection.label', "Add Selection to Chat"),
category: CHAT_CATEGORY,
icon: Codicon.attach,
f1: true,
precondition: ChatContextKeys.enabled,
menu: [{
@@ -307,9 +314,14 @@ class AttachSelectionToChatAction extends Action2 {
)
)
}, {
id: MenuId.ChatEditorInlineGutter,
group: '2_chat',
order: 1,
id: MenuId.InlineChatEditorAffordance,
group: '0_chat',
order: 2,
when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection)
}, {
id: MenuId.ChatEditorInlineMenu,
group: '0_chat',
order: 2,
when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection)
}]
});
@@ -181,7 +181,7 @@ abstract class SubmitAction extends Action2 {
}
const requestInProgressOrPendingToolCall = ContextKeyExpr.or(ChatContextKeys.requestInProgress, ChatContextKeys.Editing.hasToolConfirmation);
const whenNotInProgress = ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), ChatContextKeys.Editing.hasToolConfirmation.negate());
const whenNotInProgress = ChatContextKeys.requestInProgress.negate();
export class ChatSubmitAction extends SubmitAction {
static readonly ID = 'workbench.action.chat.submit';
@@ -13,17 +13,12 @@ import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contex
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { ChatRequestQueueKind, IChatService } from '../../common/chatService/chatService.js';
import { ChatConfiguration } from '../../common/constants.js';
import { isRequestVM } from '../../common/model/chatViewModel.js';
import { IChatWidgetService } from '../chat.js';
import { CHAT_CATEGORY } from './chatActions.js';
const queueingEnabledCondition = ContextKeyExpr.equals(`config.${ChatConfiguration.RequestQueueingEnabled}`, true);
const requestInProgressOrPendingToolCall = ContextKeyExpr.or(ChatContextKeys.requestInProgress, ChatContextKeys.Editing.hasToolConfirmation);
const queuingActionsPresent = ContextKeyExpr.and(
queueingEnabledCondition,
ContextKeyExpr.or(requestInProgressOrPendingToolCall, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.QueueOrSteer)),
ContextKeyExpr.or(ChatContextKeys.requestInProgress, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.QueueOrSteer)),
ChatContextKeys.editingRequestType.notEqualsTo(ChatContextKeys.EditingRequestType.Sent),
);
@@ -141,7 +136,6 @@ export class ChatRemovePendingRequestAction extends Action2 {
group: 'navigation',
order: 4,
when: ContextKeyExpr.and(
queueingEnabledCondition,
ChatContextKeys.isRequest,
ChatContextKeys.isPendingRequest
)
@@ -181,7 +175,6 @@ export class ChatSendPendingImmediatelyAction extends Action2 {
group: 'navigation',
order: 3,
when: ContextKeyExpr.and(
queueingEnabledCondition,
ChatContextKeys.isRequest,
ChatContextKeys.isPendingRequest
)
@@ -239,11 +232,8 @@ export class ChatRemoveAllPendingRequestsAction extends Action2 {
id: MenuId.ChatContext,
group: 'navigation',
order: 3,
when: ContextKeyExpr.and(
queueingEnabledCondition,
ChatContextKeys.hasPendingRequests
)
}]
when: ChatContextKeys.hasPendingRequests,
}],
});
}
@@ -149,7 +149,7 @@ export class PickAgentSessionAction extends Action2 {
async run(accessor: ServicesAccessor): Promise<void> {
const instantiationService = accessor.get(IInstantiationService);
const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, undefined);
const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, undefined, undefined);
await agentSessionsPicker.pickAgentSession();
}
}
@@ -11,7 +11,7 @@ import { localize } from '../../../../../nls.js';
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';
import { openSession } from './agentSessionsOpener.js';
import { ISessionOpenOptions, openSession } from './agentSessionsOpener.js';
import { IAgentSession, isLocalAgentSessionItem } from './agentSessionsModel.js';
import { IAgentSessionsService } from './agentSessionsService.js';
import { AgentSessionsSorter, groupAgentSessionsByDate, sessionDateFromNow } from './agentSessionsViewer.js';
@@ -62,12 +62,17 @@ export function getSessionButtons(session: IAgentSession): IQuickInputButton[] {
return buttons;
}
export interface IAgentSessionsPickerOptions {
overrideSessionOpen?(session: IAgentSession, openOptions?: ISessionOpenOptions): Promise<void>;
}
export class AgentSessionsPicker {
private readonly sorter = new AgentSessionsSorter();
constructor(
private readonly anchor: HTMLElement | undefined,
private readonly options: IAgentSessionsPickerOptions | undefined,
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@@ -87,13 +92,19 @@ export class AgentSessionsPicker {
disposables.add(picker.onDidAccept(e => {
const pick = picker.selectedItems[0];
if (pick) {
this.instantiationService.invokeFunction(openSession, pick.session, {
const openOptions: ISessionOpenOptions = {
sideBySide: e.inBackground,
editorOptions: {
preserveFocus: e.inBackground,
pinned: e.inBackground
}
});
};
if (this.options?.overrideSessionOpen) {
this.options.overrideSessionOpen(pick.session, openOptions);
} else {
this.instantiationService.invokeFunction(openSession, pick.session, openOptions);
}
}
if (!e.inBackground) {
@@ -758,13 +758,19 @@ export function groupAgentSessionsByDate(sessions: IAgentSession[]): Map<AgentSe
}
}
const sectionWithCount = (section: AgentSessionSection, sessions: IAgentSession[]) => ({
section,
label: localize('agentSessions.sectionWithCount', "{0} ({1})", AgentSessionSectionLabels[section], sessions.length),
sessions
});
return new Map<AgentSessionSection, IAgentSessionSection>([
[AgentSessionSection.InProgress, { section: AgentSessionSection.InProgress, label: AgentSessionSectionLabels[AgentSessionSection.InProgress], sessions: inProgressSessions }],
[AgentSessionSection.Today, { section: AgentSessionSection.Today, label: AgentSessionSectionLabels[AgentSessionSection.Today], sessions: todaySessions }],
[AgentSessionSection.Yesterday, { section: AgentSessionSection.Yesterday, label: AgentSessionSectionLabels[AgentSessionSection.Yesterday], sessions: yesterdaySessions }],
[AgentSessionSection.Week, { section: AgentSessionSection.Week, label: AgentSessionSectionLabels[AgentSessionSection.Week], sessions: weekSessions }],
[AgentSessionSection.Older, { section: AgentSessionSection.Older, label: AgentSessionSectionLabels[AgentSessionSection.Older], sessions: olderSessions }],
[AgentSessionSection.Archived, { section: AgentSessionSection.Archived, label: localize('agentSessions.archivedSectionWithCount', "Archived ({0})", archivedSessions.length), sessions: archivedSessions }],
[AgentSessionSection.InProgress, sectionWithCount(AgentSessionSection.InProgress, inProgressSessions)],
[AgentSessionSection.Today, sectionWithCount(AgentSessionSection.Today, todaySessions)],
[AgentSessionSection.Yesterday, sectionWithCount(AgentSessionSection.Yesterday, yesterdaySessions)],
[AgentSessionSection.Week, sectionWithCount(AgentSessionSection.Week, weekSessions)],
[AgentSessionSection.Older, sectionWithCount(AgentSessionSection.Older, olderSessions)],
[AgentSessionSection.Archived, sectionWithCount(AgentSessionSection.Archived, archivedSessions)],
]);
}
@@ -204,11 +204,10 @@ configurationRegistry.registerConfiguration({
},
[ChatConfiguration.AgentsControlClickBehavior]: {
type: 'string',
enum: [AgentsControlClickBehavior.Default, AgentsControlClickBehavior.Cycle, AgentsControlClickBehavior.Focus],
enum: [AgentsControlClickBehavior.Default, AgentsControlClickBehavior.Cycle],
enumDescriptions: [
nls.localize('chat.agentsControl.clickBehavior.default', "Clicking chat icon toggles chat visibility."),
nls.localize('chat.agentsControl.clickBehavior.cycle', "Clicking chat icon cycles through: show chat, maximize chat, hide chat. This requires chat to be contained in the secondary sidebar."),
nls.localize('chat.agentsControl.clickBehavior.focus', "Clicking chat icon focuses the chat view and maximizes it if located in the secondary sidebar.")
],
markdownDescription: nls.localize('chat.agentsControl.clickBehavior', "Controls the behavior when clicking on the chat icon in the command center."),
default: product.quality !== 'stable' ? AgentsControlClickBehavior.Cycle : AgentsControlClickBehavior.Default,
@@ -644,12 +643,6 @@ configurationRegistry.registerConfiguration({
enumItemLabels: ExploreAgentDefaultModel.modelLabels,
markdownEnumDescriptions: ExploreAgentDefaultModel.modelDescriptions
},
[ChatConfiguration.RequestQueueingEnabled]: {
type: 'boolean',
description: nls.localize('chat.requestQueuing.enabled.description', "When enabled, allows queuing additional messages while a request is in progress and steering the current request with a new message."),
default: true,
tags: ['experimental'],
},
[ChatConfiguration.RequestQueueingDefaultAction]: {
type: 'string',
enum: ['queue', 'steer'],
@@ -689,15 +682,10 @@ configurationRegistry.registerConfiguration({
default: true,
tags: ['experimental'],
},
['chat.statusWidget.sku']: {
type: 'string',
enum: ['free', 'anonymous'],
enumDescriptions: [
nls.localize('chat.statusWidget.sku.free', "Show status widget for free tier users."),
nls.localize('chat.statusWidget.sku.anonymous', "Show status widget for anonymous users.")
],
description: nls.localize('chat.statusWidget.enabled.description', "Controls which user type should see the status widget in new chat sessions when quota is exceeded."),
default: undefined,
['chat.statusWidget.anonymous']: {
type: 'boolean',
description: nls.localize('chat.statusWidget.anonymous.description', "Controls whether anonymous users see the status widget in new chat sessions when rate limited."),
default: false,
tags: ['experimental', 'advanced'],
experiment: {
mode: 'auto'
@@ -10,7 +10,7 @@ import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../platfor
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { IChatEditingService, IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';
import { MenuId } from '../../../../../platform/actions/common/actions.js';
import { ActionViewItem, IBaseActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js';
import { ActionViewItem, IActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js';
import { IAction, IActionRunner } from '../../../../../base/common/actions.js';
import { $, addDisposableGenericMouseMoveListener, append } from '../../../../../base/browser/dom.js';
import { assertType } from '../../../../../base/common/types.js';
@@ -35,7 +35,7 @@ export class ChatEditingAcceptRejectActionViewItem extends ActionViewItem {
constructor(
action: IAction,
options: IBaseActionViewItemOptions,
options: IActionViewItemOptions,
private readonly _entry: IObservable<IModifiedFileEntry | undefined>,
private readonly _editor: { focus(): void } | undefined,
private readonly _keybindingService: IKeybindingService,
@@ -92,7 +92,7 @@ export class ChatEditingAcceptRejectActionViewItem extends ActionViewItem {
protected override getTooltip(): string | undefined {
const value = super.getTooltip();
if (!value || this.options.keybinding) {
if (!value) {
return value;
}
return this._keybindingService.appendKeybinding(value, this._action.id);
@@ -34,6 +34,7 @@ import { IChatResponseModel } from '../../common/model/chatModel.js';
import { ChatAgentLocation } from '../../common/constants.js';
import { IDocumentDiff2 } from './chatEditingCodeEditorIntegration.js';
import { pendingRewriteMinimap } from './chatEditingModifiedFileEntry.js';
import { chatSessionResourceToId } from '../../common/model/chatUri.js';
type affectedLines = { linesAdded: number; linesRemoved: number; lineCount: number; hasRemainingEdits: boolean };
type acceptedOrRejectedLines = affectedLines & { state: 'accepted' | 'rejected' };
@@ -260,7 +261,7 @@ export class ChatEditingTextModelChangeService extends Disposable {
return EditSources.unknown({ name: 'editSessionUndoRedo' });
}
const sessionId = responseModel.session.sessionId;
const sessionId = chatSessionResourceToId(responseModel.session.sessionResource);
const request = responseModel.session.getRequests().at(-1);
const languageId = this.modifiedModel.getLanguageId();
const agent = responseModel.agent;

Some files were not shown because too many files have changed in this diff Show More