diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f7e3481c75b0c..fc5cda5555b2d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,3 +8,17 @@ updates: directory: "/" schedule: interval: "weekly" + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + allow: + - dependency-name: "@vscode/component-explorer" + - dependency-name: "@vscode/component-explorer-cli" + - package-ecosystem: "npm" + directory: "/build/vite" + schedule: + interval: "daily" + allow: + - dependency-name: "@vscode/component-explorer" + - dependency-name: "@vscode/component-explorer-vite-plugin" diff --git a/.github/hooks/hooks.json b/.github/hooks/hooks.json index 59c170e420e01..3e0f178b02329 100644 --- a/.github/hooks/hooks.json +++ b/.github/hooks/hooks.json @@ -4,7 +4,19 @@ "sessionStart": [ { "type": "command", - "bash": "if [ -f ~/.vscode-worktree-setup ]; then nohup npm ci > /tmp/npm-ci-$(date +%Y-%m-%d_%H-%M-%S).log 2>&1 & fi" + "bash": "if [ -f ~/.vscode-worktree-setup ]; then nohup bash -c 'npm ci && npm run compile' > /tmp/worktree-setup-$(date +%Y-%m-%d_%H-%M-%S).log 2>&1 & fi" + } + ], + "sessionEnd": [ + { + "type": "command", + "bash": "" + } + ], + "agentStop": [ + { + "type": "command", + "bash": "" } ], "userPromptSubmitted": [ @@ -26,4 +38,4 @@ } ] } -} \ No newline at end of file +} diff --git a/.github/skills/component-fixtures/SKILL.md b/.github/skills/component-fixtures/SKILL.md index ec2df9d4e923d..6c7eb5a6059dc 100644 --- a/.github/skills/component-fixtures/SKILL.md +++ b/.github/skills/component-fixtures/SKILL.md @@ -30,7 +30,7 @@ src/vs/workbench/test/browser/componentFixtures/ ```typescript import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'myFeature/' }, { Default: defineComponentFixture({ render: renderMyComponent }), AnotherVariant: defineComponentFixture({ render: renderMyComponent }), }); diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e950c75d9125a..9e9cc12ca99ca 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -241,6 +241,19 @@ "inSessions": true, "problemMatcher": [] }, + { + "label": "Run and Compile Dev Sessions", + "type": "shell", + "command": "npm run transpile-client && ./scripts/code.sh", + "windows": { + "command": "npm run transpile-client && .\\scripts\\code.bat" + }, + "args": [ + "--sessions" + ], + "inSessions": true, + "problemMatcher": [] + }, { "type": "npm", "script": "electron", diff --git a/build/darwin/dmg-settings.py.template b/build/darwin/dmg-settings.py.template index 4a54a69ab0264..f471029f32a2a 100644 --- a/build/darwin/dmg-settings.py.template +++ b/build/darwin/dmg-settings.py.template @@ -6,8 +6,9 @@ format = 'ULMO' badge_icon = {{BADGE_ICON}} background = {{BACKGROUND}} -# Volume size (None = auto-calculate) -size = None +# Volume size +size = '1g' +shrink = False # Files and symlinks files = [{{APP_PATH}}] diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index c22758027d155..b499fd720ff74 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -237,6 +237,9 @@ function runTsGoTypeCheck(): Promise { } const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; +const isCI = !!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY'] || !!process.env['GITHUB_WORKSPACE']; +const useCdnSourceMapsForPackagingTasks = isCI; +const stripSourceMapsInPackagingTasks = isCI; const minifyVSCodeTask = task.define('minify-vscode', task.series( bundleVSCodeTask, util.rimraf('out-vscode-min'), @@ -349,8 +352,11 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d const extensions = gulp.src(['.build/extensions/**', ...platformSpecificBuiltInExtensionsExclusions], { base: '.build', dot: true }); + const sourceFilterPattern = stripSourceMapsInPackagingTasks + ? ['**', '!**/*.{js,css}.map'] + : ['**']; const sources = es.merge(src, extensions) - .pipe(filter(['**', '!**/*.{js,css}.map'], { dot: true })); + .pipe(filter(sourceFilterPattern, { dot: true })); let version = packageJson.version; const quality = (product as { quality?: string }).quality; @@ -420,8 +426,13 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d const productionDependencies = getProductionDependencies(root); const dependenciesSrc = productionDependencies.map(d => path.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`]).flat().concat('!**/*.mk'); + const depFilterPattern = ['**', `!**/${config.version}/**`, '!**/bin/darwin-arm64-87/**', '!**/package-lock.json', '!**/yarn.lock']; + if (stripSourceMapsInPackagingTasks) { + depFilterPattern.push('!**/*.{js,css}.map'); + } + const deps = gulp.src(dependenciesSrc, { base: '.', dot: true }) - .pipe(filter(['**', `!**/${config.version}/**`, '!**/bin/darwin-arm64-87/**', '!**/package-lock.json', '!**/yarn.lock', '!**/*.{js,css}.map'])) + .pipe(filter(depFilterPattern)) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, '.moduleignore'))) .pipe(util.cleanNodeModules(path.join(import.meta.dirname, `.moduleignore.${process.platform}`))) .pipe(jsFilter) @@ -701,7 +712,13 @@ BUILD_TARGETS.forEach(buildTarget => { if (useEsbuildTranspile) { const esbuildBundleTask = task.define( `esbuild-bundle${dashed(platform)}${dashed(arch)}${dashed(minified)}`, - () => runEsbuildBundle(sourceFolderName, !!minified, true, 'desktop', minified ? `${sourceMappingURLBase}/core` : undefined) + () => runEsbuildBundle( + sourceFolderName, + !!minified, + true, + 'desktop', + minified && useCdnSourceMapsForPackagingTasks ? `${sourceMappingURLBase}/core` : undefined + ) ); vscodeTask = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( copyCodiconsTask, diff --git a/build/gulpfile.vscode.web.ts b/build/gulpfile.vscode.web.ts index e9cc3720fcf7f..3e6b29adfe9fa 100644 --- a/build/gulpfile.vscode.web.ts +++ b/build/gulpfile.vscode.web.ts @@ -33,7 +33,7 @@ const quality = (product as { quality?: string }).quality; const version = (quality && quality !== 'stable') ? `${packageJson.version}-${quality}` : packageJson.version; // esbuild-based bundle for standalone web -function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean): Promise { +function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean, sourceMapBaseUrl?: string): Promise { return new Promise((resolve, reject) => { const scriptPath = path.join(REPO_ROOT, 'build/next/index.ts'); const args = [scriptPath, 'bundle', '--out', outDir, '--target', 'web']; @@ -44,6 +44,9 @@ function runEsbuildBundle(outDir: string, minify: boolean, nls: boolean): Promis if (nls) { args.push('--nls'); } + if (sourceMapBaseUrl) { + args.push('--source-map-base-url', sourceMapBaseUrl); + } const proc = cp.spawn(process.execPath, args, { cwd: REPO_ROOT, @@ -164,8 +167,9 @@ const minifyVSCodeWebTask = task.define('minify-vscode-web-OLD', task.series( gulp.task(minifyVSCodeWebTask); // esbuild-based tasks (new) +const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; const esbuildBundleVSCodeWebTask = task.define('esbuild-vscode-web', () => runEsbuildBundle('out-vscode-web', false, true)); -const esbuildBundleVSCodeWebMinTask = task.define('esbuild-vscode-web-min', () => runEsbuildBundle('out-vscode-web-min', true, true)); +const esbuildBundleVSCodeWebMinTask = task.define('esbuild-vscode-web-min', () => runEsbuildBundle('out-vscode-web-min', true, true, `${sourceMappingURLBase}/core`)); function packageTask(sourceFolderName: string, destinationFolderName: string) { const destination = path.join(BUILD_ROOT, destinationFolderName); diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 9e2eea3b858ec..1ea3723af79fa 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -971,6 +971,14 @@ "--vscode-repl-line-height", "--vscode-sash-hover-size", "--vscode-sash-size", + "--vscode-shadow-active-tab", + "--vscode-shadow-depth-x", + "--vscode-shadow-depth-y", + "--vscode-shadow-hover", + "--vscode-shadow-lg", + "--vscode-shadow-md", + "--vscode-shadow-sm", + "--vscode-shadow-xl", "--vscode-testing-coverage-lineHeight", "--vscode-editorStickyScroll-scrollableWidth", "--vscode-editorStickyScroll-foldingOpacityTransition", diff --git a/build/next/index.ts b/build/next/index.ts index 77886ad43a989..f3043f0fa1fb2 100644 --- a/build/next/index.ts +++ b/build/next/index.ts @@ -897,6 +897,13 @@ ${tslib}`, const mangleStats: { file: string; result: ConvertPrivateFieldsResult }[] = []; // Map from JS file path to pre-mangle content + edits, for source map adjustment const mangleEdits = new Map(); + // Map from JS file path to pre-NLS content + edits, for source map adjustment + const nlsEdits = new Map(); + // Defer .map files until all .js files are processed, because esbuild may + // emit the .map file in a different build result than the .js file (e.g. + // code-split chunks), and we need the NLS/mangle edits from the .js pass + // to be available when adjusting the .map. + const deferredMaps: { path: string; text: string; contents: Uint8Array }[] = []; for (const { result } of buildResults) { if (!result.outputFiles) { continue; @@ -925,7 +932,12 @@ ${tslib}`, // Apply NLS post-processing if enabled (JS only) if (file.path.endsWith('.js') && doNls && indexMap.size > 0) { - content = postProcessNLS(content, indexMap, preserveEnglish); + const preNLSCode = content; + const nlsResult = postProcessNLS(content, indexMap, preserveEnglish); + content = nlsResult.code; + if (nlsResult.edits.length > 0) { + nlsEdits.set(file.path, { preNLSCode, edits: nlsResult.edits }); + } } // Rewrite sourceMappingURL to CDN URL if configured @@ -943,16 +955,8 @@ ${tslib}`, await fs.promises.writeFile(file.path, content); } else if (file.path.endsWith('.map')) { - // Source maps may need adjustment if private fields were mangled - const jsPath = file.path.replace(/\.map$/, ''); - const editInfo = mangleEdits.get(jsPath); - if (editInfo) { - const mapJson = JSON.parse(file.text); - const adjusted = adjustSourceMap(mapJson, editInfo.preMangleCode, editInfo.edits); - await fs.promises.writeFile(file.path, JSON.stringify(adjusted)); - } else { - await fs.promises.writeFile(file.path, file.contents); - } + // Defer .map processing until all .js files have been handled + deferredMaps.push({ path: file.path, text: file.text, contents: file.contents }); } else { // Write other files (assets, etc.) as-is await fs.promises.writeFile(file.path, file.contents); @@ -961,6 +965,27 @@ ${tslib}`, bundled++; } + // Second pass: process deferred .map files now that all mangle/NLS edits + // have been collected from .js processing above. + for (const mapFile of deferredMaps) { + const jsPath = mapFile.path.replace(/\.map$/, ''); + const mangle = mangleEdits.get(jsPath); + const nls = nlsEdits.get(jsPath); + + if (mangle || nls) { + let mapJson = JSON.parse(mapFile.text); + if (mangle) { + mapJson = adjustSourceMap(mapJson, mangle.preMangleCode, mangle.edits); + } + if (nls) { + mapJson = adjustSourceMap(mapJson, nls.preNLSCode, nls.edits); + } + await fs.promises.writeFile(mapFile.path, JSON.stringify(mapJson)); + } else { + await fs.promises.writeFile(mapFile.path, mapFile.contents); + } + } + // Log mangle-privates stats if (doManglePrivates && mangleStats.length > 0) { let totalClasses = 0, totalFields = 0, totalEdits = 0, totalElapsed = 0; diff --git a/build/next/nls-plugin.ts b/build/next/nls-plugin.ts index 7be3faccf2439..9f3bfa01e35c3 100644 --- a/build/next/nls-plugin.ts +++ b/build/next/nls-plugin.ts @@ -12,6 +12,7 @@ import { analyzeLocalizeCalls, parseLocalizeKeyOrValue } from '../lib/nls-analysis.ts'; +import type { TextEdit } from './private-to-property.ts'; // ============================================================================ // Types @@ -148,12 +149,13 @@ export async function finalizeNLS( /** * Post-processes a JavaScript file to replace NLS placeholders with indices. + * Returns the transformed code and the edits applied (for source map adjustment). */ export function postProcessNLS( content: string, indexMap: Map, preserveEnglish: boolean -): string { +): { code: string; edits: readonly TextEdit[] } { return replaceInOutput(content, indexMap, preserveEnglish); } @@ -244,7 +246,7 @@ function generateNLSSourceMap( const generator = new SourceMapGenerator(); generator.setSourceContent(filePath, originalSource); - const lineCount = originalSource.split('\n').length; + const lines = originalSource.split('\n'); // Group edits by line const editsByLine = new Map(); @@ -257,7 +259,7 @@ function generateNLSSourceMap( arr.push(edit); } - for (let line = 0; line < lineCount; line++) { + for (let line = 0; line < lines.length; line++) { const smLine = line + 1; // source maps use 1-based lines // Always map start of line @@ -273,7 +275,8 @@ function generateNLSSourceMap( let cumulativeShift = 0; - for (const edit of lineEdits) { + for (let i = 0; i < lineEdits.length; i++) { + const edit = lineEdits[i]; const origLen = edit.endCol - edit.startCol; // Map start of edit: the replacement begins at the same original position @@ -285,12 +288,20 @@ function generateNLSSourceMap( cumulativeShift += edit.newLength - origLen; - // Map content after edit: columns resume with the shift applied - generator.addMapping({ - generated: { line: smLine, column: edit.endCol + cumulativeShift }, - original: { line: smLine, column: edit.endCol }, - source: filePath, - }); + // Source maps don't interpolate columns — each query resolves to the + // last segment with generatedColumn <= queryColumn. A single mapping + // at edit-end would cause every subsequent column on this line to + // collapse to that one original position. Add per-column identity + // mappings from edit-end to the next edit (or end of line) so that + // esbuild's source-map composition preserves fine-grained accuracy. + const nextBound = i + 1 < lineEdits.length ? lineEdits[i + 1].startCol : lines[line].length; + for (let origCol = edit.endCol; origCol < nextBound; origCol++) { + generator.addMapping({ + generated: { line: smLine, column: origCol + cumulativeShift }, + original: { line: smLine, column: origCol }, + source: filePath, + }); + } } } } @@ -302,17 +313,19 @@ function replaceInOutput( content: string, indexMap: Map, preserveEnglish: boolean -): string { - // Replace all placeholders in a single pass using regex - // Two types of placeholders: - // - %%NLS:moduleId#key%% for localize() - message replaced with null - // - %%NLS2:moduleId#key%% for localize2() - message preserved - // Note: esbuild may use single or double quotes, so we handle both +): { code: string; edits: readonly TextEdit[] } { + // Collect all matches first, then apply from back to front so that byte + // offsets remain valid. Each match becomes a TextEdit in terms of the + // ORIGINAL content offsets, which is what adjustSourceMap expects. + + interface PendingEdit { start: number; end: number; replacement: string } + const pending: PendingEdit[] = []; if (preserveEnglish) { - // Just replace the placeholder with the index (both NLS and NLS2) - return content.replace(/["']%%NLS2?:([^%]+)%%["']/g, (match, inner) => { - // Try NLS first, then NLS2 + const re = /["']%%NLS2?:([^%]+)%%["']/g; + let m: RegExpExecArray | null; + while ((m = re.exec(content)) !== null) { + const inner = m[1]; let placeholder = `%%NLS:${inner}%%`; let index = indexMap.get(placeholder); if (index === undefined) { @@ -320,45 +333,60 @@ function replaceInOutput( index = indexMap.get(placeholder); } if (index !== undefined) { - return String(index); + pending.push({ start: m.index, end: m.index + m[0].length, replacement: String(index) }); } - // Placeholder not found in map, leave as-is (shouldn't happen) - return match; - }); + } } else { - // For NLS (localize): replace placeholder with index AND replace message with null - // For NLS2 (localize2): replace placeholder with index, keep message - // Note: Use (?:[^"\\]|\\.)* to properly handle escaped quotes like \" or \\ - // Note: esbuild may use single or double quotes, so we handle both - - // First handle NLS (localize) - replace both key and message - content = content.replace( - /["']%%NLS:([^%]+)%%["'](\s*,\s*)(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g, - (match, inner, comma) => { - const placeholder = `%%NLS:${inner}%%`; - const index = indexMap.get(placeholder); - if (index !== undefined) { - return `${index}${comma}null`; - } - return match; + // NLS (localize): replace placeholder with index AND replace message with null + const reNLS = /["']%%NLS:([^%]+)%%["'](\s*,\s*)(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g; + let m: RegExpExecArray | null; + while ((m = reNLS.exec(content)) !== null) { + const inner = m[1]; + const comma = m[2]; + const placeholder = `%%NLS:${inner}%%`; + const index = indexMap.get(placeholder); + if (index !== undefined) { + pending.push({ start: m.index, end: m.index + m[0].length, replacement: `${index}${comma}null` }); } - ); - - // Then handle NLS2 (localize2) - replace only key, keep message - content = content.replace( - /["']%%NLS2:([^%]+)%%["']/g, - (match, inner) => { - const placeholder = `%%NLS2:${inner}%%`; - const index = indexMap.get(placeholder); - if (index !== undefined) { - return String(index); - } - return match; + } + + // NLS2 (localize2): replace only key, keep message + const reNLS2 = /["']%%NLS2:([^%]+)%%["']/g; + while ((m = reNLS2.exec(content)) !== null) { + const inner = m[1]; + const placeholder = `%%NLS2:${inner}%%`; + const index = indexMap.get(placeholder); + if (index !== undefined) { + pending.push({ start: m.index, end: m.index + m[0].length, replacement: String(index) }); } - ); + } + } - return content; + if (pending.length === 0) { + return { code: content, edits: [] }; } + + // Sort by offset ascending, then apply back-to-front to keep offsets valid + pending.sort((a, b) => a.start - b.start); + + // Build TextEdit[] (in original-content coordinates) and apply edits + const edits: TextEdit[] = []; + for (const p of pending) { + edits.push({ start: p.start, end: p.end, newText: p.replacement }); + } + + // Apply edits using forward-scanning parts array — O(N+K) instead of + // O(N*K) from repeated substring concatenation on large strings. + const parts: string[] = []; + let lastEnd = 0; + for (const p of pending) { + parts.push(content.substring(lastEnd, p.start)); + parts.push(p.replacement); + lastEnd = p.end; + } + parts.push(content.substring(lastEnd)); + + return { code: parts.join(''), edits }; } // ============================================================================ diff --git a/build/next/private-to-property.ts b/build/next/private-to-property.ts index 11f977774a5fd..98ff98a64408a 100644 --- a/build/next/private-to-property.ts +++ b/build/next/private-to-property.ts @@ -220,15 +220,53 @@ export function adjustSourceMap( return sourceMapJson; } - // Build a line-offset table for the original code to convert byte offsets to line/column - const lineStarts: number[] = [0]; - for (let i = 0; i < originalCode.length; i++) { - if (originalCode.charCodeAt(i) === 10 /* \n */) { - lineStarts.push(i + 1); + // Build line-offset tables for the original code and the code after edits. + // When edits span newlines (e.g. NLS replacing a multi-line template literal + // with `null`), subsequent lines shift up and columns change. We handle this + // by converting each mapping's old generated (line, col) to a byte offset, + // adjusting the offset for the edits, then converting back to (line, col) in + // the post-edit coordinate system. + + const oldLineStarts = buildLineStarts(originalCode); + const newLineStarts = buildLineStartsAfterEdits(originalCode, edits); + + // Precompute cumulative byte-shift after each edit for binary search + const n = edits.length; + const editStarts: number[] = new Array(n); + const editEnds: number[] = new Array(n); + const cumShifts: number[] = new Array(n); // cumulative shift *after* edit[i] + let cumShift = 0; + for (let i = 0; i < n; i++) { + editStarts[i] = edits[i].start; + editEnds[i] = edits[i].end; + cumShift += edits[i].newText.length - (edits[i].end - edits[i].start); + cumShifts[i] = cumShift; + } + + function adjustOffset(oldOff: number): number { + // Binary search: find last edit with start <= oldOff + let lo = 0, hi = n - 1; + while (lo <= hi) { + const mid = (lo + hi) >> 1; + if (editStarts[mid] <= oldOff) { + lo = mid + 1; + } else { + hi = mid - 1; + } + } + // hi = index of last edit where start <= oldOff, or -1 if none + if (hi < 0) { + return oldOff; } + if (oldOff < editEnds[hi]) { + // Inside edit range — clamp to edit start in new coordinates + const prevShift = hi > 0 ? cumShifts[hi - 1] : 0; + return editStarts[hi] + prevShift; + } + return oldOff + cumShifts[hi]; } - function offsetToLineCol(offset: number): { line: number; col: number } { + function offsetToLineCol(lineStarts: readonly number[], offset: number): { line: number; col: number } { let lo = 0, hi = lineStarts.length - 1; while (lo < hi) { const mid = (lo + hi + 1) >> 1; @@ -241,23 +279,9 @@ export function adjustSourceMap( return { line: lo, col: offset - lineStarts[lo] }; } - // Convert edits from byte offsets to per-line column shifts - interface LineEdit { col: number; origLen: number; newLen: number } - const editsByLine = new Map(); - for (const edit of edits) { - const pos = offsetToLineCol(edit.start); - const origLen = edit.end - edit.start; - let arr = editsByLine.get(pos.line); - if (!arr) { - arr = []; - editsByLine.set(pos.line, arr); - } - arr.push({ col: pos.col, origLen, newLen: edit.newText.length }); - } - // Use source-map library to read, adjust, and write const consumer = new SourceMapConsumer(sourceMapJson); - const generator = new SourceMapGenerator({ file: sourceMapJson.file }); + const generator = new SourceMapGenerator({ file: sourceMapJson.file, sourceRoot: sourceMapJson.sourceRoot }); // Copy sourcesContent for (let i = 0; i < sourceMapJson.sources.length; i++) { @@ -267,15 +291,19 @@ export function adjustSourceMap( } } - // Walk every mapping, adjust the generated column, and add to the new generator + // Walk every mapping, convert old generated position → byte offset → adjust → new position consumer.eachMapping(mapping => { - const lineEdits = editsByLine.get(mapping.generatedLine - 1); // 0-based for our data - const adjustedCol = adjustColumn(mapping.generatedColumn, lineEdits); + const oldLine0 = mapping.generatedLine - 1; // 0-based + const oldOff = (oldLine0 < oldLineStarts.length + ? oldLineStarts[oldLine0] + : oldLineStarts[oldLineStarts.length - 1]) + mapping.generatedColumn; + + const newOff = adjustOffset(oldOff); + const newPos = offsetToLineCol(newLineStarts, newOff); - // Some mappings may be unmapped (no original position/source) - skip those. if (mapping.source !== null && mapping.originalLine !== null && mapping.originalColumn !== null) { const newMapping: Mapping = { - generated: { line: mapping.generatedLine, column: adjustedCol }, + generated: { line: newPos.line + 1, column: newPos.col }, original: { line: mapping.originalLine, column: mapping.originalColumn }, source: mapping.source, }; @@ -283,25 +311,82 @@ export function adjustSourceMap( newMapping.name = mapping.name; } generator.addMapping(newMapping); + } else { + // Preserve unmapped segments (generated-only mappings with no original + // position). These create essential "gaps" that prevent + // originalPositionFor() from wrongly interpolating between distant + // valid mappings on the same line in minified output. + // eslint-disable-next-line local/code-no-dangerous-type-assertions + generator.addMapping({ + generated: { line: newPos.line + 1, column: newPos.col }, + } as Mapping); } }); return JSON.parse(generator.toString()); } -function adjustColumn(col: number, lineEdits: { col: number; origLen: number; newLen: number }[] | undefined): number { - if (!lineEdits) { - return col; +function buildLineStarts(text: string): number[] { + const starts: number[] = [0]; + let pos = 0; + while (true) { + const nl = text.indexOf('\n', pos); + if (nl === -1) { + break; + } + starts.push(nl + 1); + pos = nl + 1; + } + return starts; +} + +/** + * Compute line starts for the code that results from applying `edits` to + * `originalCode`, without materialising the full new string. + */ +function buildLineStartsAfterEdits(originalCode: string, edits: readonly TextEdit[]): number[] { + const starts: number[] = [0]; + let oldPos = 0; + let newPos = 0; + + for (const edit of edits) { + // Scan unchanged region [oldPos, edit.start) for newlines + let from = oldPos; + while (true) { + const nl = originalCode.indexOf('\n', from); + if (nl === -1 || nl >= edit.start) { + break; + } + starts.push(newPos + (nl - oldPos) + 1); + from = nl + 1; + } + newPos += edit.start - oldPos; + + // Scan replacement text for newlines + let replFrom = 0; + while (true) { + const nl = edit.newText.indexOf('\n', replFrom); + if (nl === -1) { + break; + } + starts.push(newPos + nl + 1); + replFrom = nl + 1; + } + newPos += edit.newText.length; + + oldPos = edit.end; } - let shift = 0; - for (const edit of lineEdits) { - if (edit.col + edit.origLen <= col) { - shift += edit.newLen - edit.origLen; - } else if (edit.col < col) { - return edit.col + shift; - } else { + + // Scan remaining unchanged text after last edit + let from = oldPos; + while (true) { + const nl = originalCode.indexOf('\n', from); + if (nl === -1) { break; } + starts.push(newPos + (nl - oldPos) + 1); + from = nl + 1; } - return col + shift; + + return starts; } diff --git a/build/next/test/nls-sourcemap.test.ts b/build/next/test/nls-sourcemap.test.ts index fd732b8680217..09bad2f5c279e 100644 --- a/build/next/test/nls-sourcemap.test.ts +++ b/build/next/test/nls-sourcemap.test.ts @@ -10,6 +10,7 @@ import * as fs from 'fs'; import * as os from 'os'; import { type RawSourceMap, SourceMapConsumer } from 'source-map'; import { nlsPlugin, createNLSCollector, finalizeNLS, postProcessNLS } from '../nls-plugin.ts'; +import { adjustSourceMap } from '../private-to-property.ts'; // analyzeLocalizeCalls requires the import path to end with `/nls` const NLS_STUB = [ @@ -36,7 +37,7 @@ interface BundleResult { async function bundleWithNLS( files: Record, entryPoint: string, - opts?: { postProcess?: boolean } + opts?: { postProcess?: boolean; minify?: boolean } ): Promise { const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'nls-sm-test-')); const srcDir = path.join(tmpDir, 'src'); @@ -64,6 +65,7 @@ async function bundleWithNLS( packages: 'external', sourcemap: 'linked', sourcesContent: true, + minify: opts?.minify ?? false, write: false, plugins: [ nlsPlugin({ baseDir: srcDir, collector }), @@ -91,7 +93,16 @@ async function bundleWithNLS( // Optionally apply NLS post-processing (replaces placeholders with indices) if (opts?.postProcess) { const nlsResult = await finalizeNLS(collector, outDir); - jsContent = postProcessNLS(jsContent, nlsResult.indexMap, false); + const preNLSCode = jsContent; + const nlsProcessed = postProcessNLS(jsContent, nlsResult.indexMap, false); + jsContent = nlsProcessed.code; + + // Adjust source map for NLS edits + if (nlsProcessed.edits.length > 0) { + const mapJson = JSON.parse(mapContent); + const adjusted = adjustSourceMap(mapJson, preNLSCode, nlsProcessed.edits); + mapContent = JSON.stringify(adjusted); + } } assert.ok(jsContent, 'Expected JS output'); @@ -370,4 +381,82 @@ suite('NLS plugin source maps', () => { cleanup(); } }); + + test('post-processed NLS - column mappings correct after placeholder replacement', async () => { + // NLS placeholders like "%%NLS:test/drift#k%%" are much longer than their + // replacements (e.g. "0"). Without source map adjustment the columns for + // tokens AFTER the replacement drift by the cumulative length delta. + const source = [ + 'import { localize } from "../vs/nls";', // 1 + 'export const a = localize("k1", "Alpha"); export const MARKER = "FINDME";', // 2 + ].join('\n'); + + const { js, map, cleanup } = await bundleWithNLS( + { 'test/drift.ts': source }, + 'test/drift.ts', + { postProcess: true } + ); + + try { + assert.ok(!js.includes('%%NLS:'), 'Placeholders should be replaced'); + + const bundleLine = findLine(js, 'FINDME'); + const bundleCol = findColumn(js, '"FINDME"'); + const pos = map.originalPositionFor({ line: bundleLine, column: bundleCol }); + + assert.ok(pos.source, 'Should have source'); + assert.strictEqual(pos.line, 2, 'Should map to line 2'); + + const originalCol = findColumn(source, '"FINDME"'); + const columnDrift = Math.abs(pos.column! - originalCol); + assert.ok(columnDrift <= 20, + `Column drift after NLS post-processing should be small. ` + + `Expected ~${originalCol}, got ${pos.column} (drift: ${columnDrift}). ` + + `Large drift means postProcessNLS edits were not applied to the source map.`); + } finally { + cleanup(); + } + }); + + test('minified bundle with NLS - end-to-end column mapping', async () => { + // With minification, the entire output is (roughly) on one line. + // Multiple NLS replacements compound their column shifts. A function + // defined after several localize() calls must still map correctly. + const source = [ + 'import { localize } from "../vs/nls";', // 1 + '', // 2 + 'export const a = localize("k1", "Alpha message");', // 3 + 'export const b = localize("k2", "Bravo message that is quite long");', // 4 + 'export const c = localize("k3", "Charlie");', // 5 + 'export const d = localize("k4", "Delta is the fourth letter");', // 6 + '', // 7 + 'export function computeResult(x: number): number {', // 8 + '\treturn x * 42;', // 9 + '}', // 10 + ].join('\n'); + + const { js, map, cleanup } = await bundleWithNLS( + { 'test/minified.ts': source }, + 'test/minified.ts', + { postProcess: true, minify: true } + ); + + try { + assert.ok(!js.includes('%%NLS:'), 'Placeholders should be replaced'); + + // Find the computeResult function in the minified output. + // esbuild minifies `x * 42` and may rename the parameter, so + // search for `*42` which survives both minification and renaming. + const needle = '*42'; + const bundleLine = findLine(js, needle); + const bundleCol = findColumn(js, needle); + const pos = map.originalPositionFor({ line: bundleLine, column: bundleCol }); + + assert.ok(pos.source, 'Should have source for minified mapping'); + assert.strictEqual(pos.line, 9, + `Should map "*42" back to line 9. Got line ${pos.line}.`); + } finally { + cleanup(); + } + }); }); diff --git a/build/next/test/private-to-property.test.ts b/build/next/test/private-to-property.test.ts index aa9da72ce9a51..9b97679767933 100644 --- a/build/next/test/private-to-property.test.ts +++ b/build/next/test/private-to-property.test.ts @@ -439,6 +439,41 @@ suite('adjustSourceMap', () => { assert.strictEqual(pos.column, origGetValueCol, 'getValue column should match original'); }); + test('multi-line edit: removing newlines shifts subsequent lines up', () => { + // Simulates the NLS scenario: a template literal with embedded newlines + // is replaced with `null`, collapsing 3 lines into 1. + const code = [ + 'var a = "hello";', // line 0 (0-based) + 'var b = `line1', // line 1 + 'line2', // line 2 + 'line3`;', // line 3 + 'var c = "world";', // line 4 + ].join('\n'); + const map = createIdentitySourceMap(code, 'test.js'); + + // Replace the template literal `line1\nline2\nline3` with `null` + // (keeps `var b = ` and `;` intact) + const tplStart = code.indexOf('`line1'); + const tplEnd = code.indexOf('line3`') + 'line3`'.length; + const edits = [{ start: tplStart, end: tplEnd, newText: 'null' }]; + + const result = adjustSourceMap(map, code, edits); + const consumer = new SourceMapConsumer(result); + + // After edit, code is: + // "var a = \"hello\";\nvar b = null;\nvar c = \"world\";" + // "var c" was on line 5 (1-based), now on line 3 (1-based) since 2 newlines removed + + // 'var c' at original line 5, col 0 should now map at generated line 3 + const pos = consumer.originalPositionFor({ line: 3, column: 0 }); + assert.strictEqual(pos.line, 5, 'var c should map to original line 5'); + assert.strictEqual(pos.column, 0, 'var c column should be 0'); + + // 'var a' on line 1 should be unaffected + const posA = consumer.originalPositionFor({ line: 1, column: 0 }); + assert.strictEqual(posA.line, 1, 'var a should still map to original line 1'); + }); + test('brand check: #field in obj -> string replacement adjusts map', () => { const code = 'class C { #x; check(o) { return #x in o; } }'; const map = createIdentitySourceMap(code, 'test.js'); diff --git a/build/next/working.md b/build/next/working.md index 298d1fb8cbd48..a7ea64db8b648 100644 --- a/build/next/working.md +++ b/build/next/working.md @@ -222,13 +222,13 @@ Two categories of corruption: 2. **`--source-map-base-url` option** - Rewrites `sourceMappingURL` comments to point to CDN URLs. -3. **NLS plugin inline source maps** (`nls-plugin.ts`) - The `onLoad` handler now generates an inline source map (`//# sourceMappingURL=data:...`) mapping from NLS-transformed source back to original. esbuild composes this with its own bundle source map. `SourceMapGenerator.setSourceContent` embeds the original source so `sourcesContent` in the final `.map` has the real TypeScript. Tests in `test/nls-sourcemap.test.ts`. +3. **NLS plugin inline source maps** (`nls-plugin.ts`) - The `onLoad` handler generates an inline source map (`//# sourceMappingURL=data:...`) mapping from NLS-transformed source back to original. esbuild composes this with its own bundle source map. `SourceMapGenerator.setSourceContent` embeds the original source so `sourcesContent` in the final `.map` has the real TypeScript. `generateNLSSourceMap` adds per-column identity mappings after each edit on a line so that esbuild's source-map composition preserves fine-grained column accuracy (source maps don't interpolate columns — they use binary search, so a single boundary mapping would collapse all subsequent columns to the edit-end position). Tests in `test/nls-sourcemap.test.ts`. 4. **`convertPrivateFields` source map adjustment** (`private-to-property.ts`) - `convertPrivateFields` returns its sorted edits as `TextEdit[]`. `adjustSourceMap()` uses `SourceMapConsumer` to walk every mapping, adjusts generated columns based on cumulative edit shifts per line, and rebuilds with `SourceMapGenerator`. The post-processing loop in `index.ts` saves pre-mangle content + edits per JS file, then applies `adjustSourceMap` to the corresponding `.map`. Tests in `test/private-to-property.test.ts`. -### Not Yet Fixed +5. **`postProcessNLS` source map adjustment** (`nls-plugin.ts`, `index.ts`) — `postProcessNLS` now returns `{ code, edits }` where `edits` is a `TextEdit[]` tracking each replacement's byte offset. The bundle loop in `index.ts` chains `adjustSourceMap` calls: first for mangle edits, then for NLS edits, so both transforms are accurately reflected in the final `.map` file. Tests in `test/nls-sourcemap.test.ts`. -**`postProcessNLS` column drift** - Replaces NLS placeholders with short indices in bundled output without updating `.map` files. Shifts columns but never lines, so line-level debugging and crash reporting work correctly. Fixing would require tracking replacement offsets through regex matches and adjusting the source map, similar to `adjustSourceMap`. +6. **`adjustSourceMap` unmapped segment preservation** (`private-to-property.ts`) — Previously, `adjustSourceMap()` silently dropped mappings where `source === null`. These unmapped segments create essential "gaps" that prevent `originalPositionFor()` from wrongly interpolating between distant valid mappings on the same minified line. Now emits them as generated-only mappings. Also preserves `sourceRoot` from the input map. ### Key Technical Details @@ -241,6 +241,71 @@ Two categories of corruption: **Plugin interaction:** Both the NLS plugin and `fileContentMapperPlugin` register `onLoad({ filter: /\.ts$/ })`. In esbuild, the first `onLoad` to return non-`undefined` wins. The NLS plugin is `unshift`ed (runs first), so files with NLS calls skip `fileContentMapperPlugin`. This is safe in practice since `product.ts` (which has `BUILD->INSERT_PRODUCT_CONFIGURATION`) has no localize calls. +### Still Broken — Full Production Build (`npm run gulp vscode-min`) + +**Symptom:** Source maps are totally broken in the minified production build. E.g. a breakpoint at `src/vs/editor/browser/editorExtensions.ts` line 308 resolves to `src/vs/editor/common/cursor/cursorMoveCommands.ts` line 732 — a completely different file. This is **cross-file** mapping corruption, not just column drift. + +**Status of unit tests:** The fixes above pass in isolated unit tests (small 1–2 file bundles via `esbuild.build` with `minify: true`). The tests verify column drift ≤ 20 and correct line mapping for single-file bundles with NLS. **183 tests pass, 0 failing.** But the full production build bundles hundreds of files into huge minified outputs (e.g. `workbench.desktop.main.js` at ~15 MB) and the source maps break at that scale. + +**Suspected root causes (need investigation):** + +1. **`generateNLSSourceMap` per-column identity mappings may overwhelm esbuild's source-map composition.** The fix added one mapping per column from edit-end to end-of-line (or next edit). For a long TypeScript line with a `localize()` call near the beginning, this generates hundreds of identity mappings per line. Across hundreds of files, the inline source maps embedded in `onLoad` responses may be extremely large. esbuild must compose these with its own source maps during bundling — it may hit limits, silently drop mappings, or produce incorrect composed maps at this scale. **Mitigation to try:** Instead of per-column mappings, use sparser "checkpoint" mappings (e.g., every N characters) or rely only on boundary mappings and accept some column drift within the NLS-transformed region. The old boundary-only approach was wrong (collapsed all downstream columns), but per-column may be the other extreme. + +2. **`adjustSourceMap` may corrupt source indices in large minified bundles.** In a minified bundle, the entire output is on one or very few lines. `adjustSourceMap()` walks every mapping via `SourceMapConsumer.eachMapping()` and adjusts `generatedColumn` using `adjustColumn()`. But when thousands of mappings all share `generatedLine: 1` and there are hundreds of NLS edits on that same line, there may be sorting/ordering bugs: `eachMapping()` returns mappings in generated order by default, but `adjustColumn()` binary-searches through edits sorted by column. If edits cover regions that interleave with mappings from different source files, the cumulative shift calculation might produce wrong columns that then resolve to wrong source files. + +3. **Chained `adjustSourceMap` calls (mangle → NLS) may compound errors.** After the first `adjustSourceMap` for mangle edits, the source map's generated columns are updated. The second call for NLS edits uses `nlsEdits` which were computed against `preNLSCode` — but `preNLSCode` is the post-mangle JS, which is what the first `adjustSourceMap` maps from. This chaining _should_ be correct, but needs verification at scale with a real minified bundle. + +4. **The `source-map` v0.6.1 library may have precision issues with very large VLQ-encoded maps.** The bundled outputs have source maps with hundreds of thousands of mappings. The library is old (2017) and there may be numerical precision or sorting issues with very large maps. Consider testing with `source-map` v0.7+ or the Rust-based `@aspect-build/source-map`. + +5. **Alternative approach: skip per-column NLS plugin mappings, fix only `postProcessNLS`.** The NLS plugin `onLoad` replaces `"key"` with `"%%NLS:longPlaceholder%%"` — a length change that only affects columns on affected lines. The subsequent `postProcessNLS` then replaces the long placeholder with a short index. If the `adjustSourceMap` for `postProcessNLS` is correct, it should compensate for both expansions (plugin expansion + post-process contraction). We might not need per-column mappings in `generateNLSSourceMap` at all — just the boundary mapping. The column will drift in the intermediate representation but `adjustSourceMap` for NLS should fix it. **This hypothesis needs testing.** + +6. **Alternative approach: do NLS replacement purely in post-processing.** Skip the `onLoad` two-phase approach (placeholder insertion + post-processing replacement) entirely. Instead, run `postProcessNLS` as a single post-processing step that directly replaces `localize("key", "message")` → `localize(0, null)` in the bundled JS output, with proper source-map adjustment via `adjustSourceMap`. This avoids both the inline source map composition complexity and the two-step replacement. The downside is that post-processing must parse/regex-match real `localize()` calls (not easy placeholders), which is more fragile. + +**Summary of fixes applied vs status:** + +| Bug | Fix | Unit test | Production | +|-----|-----|-----------|------------| +| `generateNLSSourceMap` only had boundary mappings → columns collapsed | Added per-column identity mappings after each edit | Pass (drift: 0) | **Broken** — may overwhelm esbuild composition at scale | +| `postProcessNLS` didn't track edits for source map adjustment | Returns `{ code, edits }`, chained in `index.ts` | Pass | **Broken** — `adjustSourceMap` may corrupt source indices on huge single-line minified output | +| `adjustSourceMap` dropped unmapped segments | Preserves generated-only mappings + `sourceRoot` | Pass (no regressions) | **Broken** — same cross-file mapping issue | + +**Files involved:** +- `build/next/nls-plugin.ts` — `generateNLSSourceMap()` (per-column mappings), `postProcessNLS()` (returns edits), `replaceInOutput()` (regex replacement) +- `build/next/private-to-property.ts` — `adjustSourceMap()` (column adjustment) +- `build/next/index.ts` — bundle post-processing loop (lines ~899–975), chains adjustSourceMap calls +- `build/next/test/nls-sourcemap.test.ts` — unit tests (pass but don't cover production-scale bundles) + +**How to reproduce:** +```bash +npm run gulp vscode-min +# Open out-vscode-min/ in a debugger, set breakpoints in editor files +# Observe breakpoints resolve to wrong files +``` + +**How to debug further:** +```bash +# 1. Build with just --nls (no mangle) to isolate NLS from mangle issues +npx tsx build/next/index.ts bundle --nls --minify --target desktop --out out-debug + +# 2. Build with just --mangle-privates (no NLS) to isolate mangle issues +npx tsx build/next/index.ts bundle --mangle-privates --minify --target desktop --out out-debug + +# 3. Build with neither (baseline — does esbuild's own map work?) +npx tsx build/next/index.ts bundle --minify --target desktop --out out-debug + +# 4. Compare .map files across the three builds to find where mappings diverge + +# 5. Validate a specific mapping in the large bundle: +node -e " +const {SourceMapConsumer} = require('source-map'); +const fs = require('fs'); +const map = JSON.parse(fs.readFileSync('./out-debug/vs/workbench/workbench.desktop.main.js.map','utf8')); +const c = new SourceMapConsumer(map); +// Look up a known position and see which source file it resolves to +console.log(c.originalPositionFor({line: 1, column: XXXX})); +" +``` + --- ## Self-hosting Setup diff --git a/build/npm/installStateHash.ts b/build/npm/installStateHash.ts index 5674a1eaee377..1ee80522d6ca1 100644 --- a/build/npm/installStateHash.ts +++ b/build/npm/installStateHash.ts @@ -36,15 +36,46 @@ export interface PostinstallState { readonly fileHashes: Record; } -const packageJsonIgnoredKeys = new Set(['distro']); +const packageJsonRelevantKeys = new Set([ + 'name', + 'dependencies', + 'devDependencies', + 'optionalDependencies', + 'peerDependencies', + 'peerDependenciesMeta', + 'overrides', + 'engines', + 'workspaces', + 'bundledDependencies', + 'bundleDependencies', +]); + +const packageLockJsonIgnoredKeys = new Set(['version']); function normalizeFileContent(filePath: string): string { const raw = fs.readFileSync(filePath, 'utf8'); - if (path.basename(filePath) === 'package.json') { + const basename = path.basename(filePath); + if (basename === 'package.json') { const json = JSON.parse(raw); - for (const key of packageJsonIgnoredKeys) { + const filtered: Record = {}; + for (const key of packageJsonRelevantKeys) { + // eslint-disable-next-line local/code-no-in-operator + if (key in json) { + filtered[key] = json[key]; + } + } + return JSON.stringify(filtered, null, '\t') + '\n'; + } + if (basename === 'package-lock.json') { + const json = JSON.parse(raw); + for (const key of packageLockJsonIgnoredKeys) { delete json[key]; } + if (json.packages?.['']) { + for (const key of packageLockJsonIgnoredKeys) { + delete json.packages[''][key]; + } + } return JSON.stringify(json, null, '\t') + '\n'; } return raw; diff --git a/build/vite/package-lock.json b/build/vite/package-lock.json index de462673a18b1..b7e27044aefbd 100644 --- a/build/vite/package-lock.json +++ b/build/vite/package-lock.json @@ -8,8 +8,8 @@ "name": "@vscode/sample-source", "version": "0.0.0", "devDependencies": { - "@vscode/component-explorer": "^0.1.1-16", - "@vscode/component-explorer-vite-plugin": "^0.1.1-16", + "@vscode/component-explorer": "^0.1.1-19", + "@vscode/component-explorer-vite-plugin": "^0.1.1-19", "@vscode/rollup-plugin-esm-url": "^1.0.1-1", "rollup": "*", "vite": "npm:rolldown-vite@latest" @@ -683,9 +683,9 @@ "license": "MIT" }, "node_modules/@vscode/component-explorer": { - "version": "0.1.1-16", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-16.tgz", - "integrity": "sha512-is1RxdlNO5K1RSqWd5z8BN6gPrqEBZfjgUi3ZJbQj8Z4VqmqoJsNLIzBXOIlQJX+5mWgeNdOq3vxe0u15ZkAlA==", + "version": "0.1.1-19", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-19.tgz", + "integrity": "sha512-wvcjw1A8wSH/oR5q+lZrBSyOQZfvXtLPYkQJBj11FBKu35iHko0FTIPMG25Ee+TpT2/BWLd29dWwiJODDQbC8w==", "dev": true, "license": "MIT", "dependencies": { @@ -694,9 +694,9 @@ } }, "node_modules/@vscode/component-explorer-vite-plugin": { - "version": "0.1.1-16", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer-vite-plugin/-/component-explorer-vite-plugin-0.1.1-16.tgz", - "integrity": "sha512-z2EqusWl49dUF3vNDgmJJJQXkv4ejeBH9AdFZUWOiGaMvjjFX6UV7oQ733b+vo5YFE8my9WaK7D691i2wZ47Fg==", + "version": "0.1.1-19", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer-vite-plugin/-/component-explorer-vite-plugin-0.1.1-19.tgz", + "integrity": "sha512-V0wMhLvHMbeUHOzwGrBPMwwvcbGhXXaQTCGc9hNfF4fjUutOtQFu5o+9XKDG1hIcKgk5qyvcRoXjVazBcg19lA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/build/vite/package.json b/build/vite/package.json index 5e5d59d1a1696..14f6ad51c578e 100644 --- a/build/vite/package.json +++ b/build/vite/package.json @@ -9,8 +9,8 @@ "preview": "vite preview" }, "devDependencies": { - "@vscode/component-explorer": "^0.1.1-16", - "@vscode/component-explorer-vite-plugin": "^0.1.1-16", + "@vscode/component-explorer": "^0.1.1-19", + "@vscode/component-explorer-vite-plugin": "^0.1.1-19", "@vscode/rollup-plugin-esm-url": "^1.0.1-1", "rollup": "*", "vite": "npm:rolldown-vite@latest" diff --git a/extensions/css/cgmanifest.json b/extensions/css/cgmanifest.json index 7b85089b6b91a..93bd8ba0f3109 100644 --- a/extensions/css/cgmanifest.json +++ b/extensions/css/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "microsoft/vscode-css", "repositoryUrl": "https://github.com/microsoft/vscode-css", - "commitHash": "a927fe2f73927bf5c25d0b0c4dd0e63d69fd8887" + "commitHash": "9a07d76cb0e7a56f9bfc76328a57227751e4adb4" } }, "licenseDetail": [ diff --git a/extensions/css/syntaxes/css.tmLanguage.json b/extensions/css/syntaxes/css.tmLanguage.json index 5ba8bc90b7381..484af027c195c 100644 --- a/extensions/css/syntaxes/css.tmLanguage.json +++ b/extensions/css/syntaxes/css.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/microsoft/vscode-css/commit/a927fe2f73927bf5c25d0b0c4dd0e63d69fd8887", + "version": "https://github.com/microsoft/vscode-css/commit/9a07d76cb0e7a56f9bfc76328a57227751e4adb4", "name": "CSS", "scopeName": "source.css", "patterns": [ @@ -1401,7 +1401,7 @@ "property-keywords": { "patterns": [ { - "match": "(?xi) (? un interface RunConfig { readonly platform: 'node' | 'browser'; - readonly format?: 'cjs' | 'esm'; readonly srcDir: string; readonly outdir: string; readonly entryPoints: string[] | Record | { in: string; out: string }[]; @@ -49,7 +48,6 @@ function resolveOptions(config: RunConfig, outdir: string): BuildOptions { sourcemap: true, target: ['es2024'], external: ['vscode'], - format: config.format ?? 'cjs', entryPoints: config.entryPoints, outdir, logOverride: { @@ -59,8 +57,10 @@ function resolveOptions(config: RunConfig, outdir: string): BuildOptions { }; if (config.platform === 'node') { + options.format = 'cjs'; options.mainFields = ['module', 'main']; } else if (config.platform === 'browser') { + options.format = 'cjs'; options.mainFields = ['browser', 'module', 'main']; options.alias = { 'path': 'path-browserify', diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index abe5c33107422..e5820c0ded74a 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -347,6 +347,10 @@ export class ApiRepository implements Repository { migrateChanges(sourceRepositoryPath: string, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise { return this.#repository.migrateChanges(sourceRepositoryPath, options); } + + generateRandomBranchName(): Promise { + return this.#repository.generateRandomBranchName(); + } } export class ApiGit implements Git { diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 287dd4399bf2c..122134c2c8b57 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -325,6 +325,8 @@ export interface Repository { deleteWorktree(path: string, options?: { force?: boolean }): Promise; migrateChanges(sourceRepositoryPath: string, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise; + + generateRandomBranchName(): Promise; } export interface RemoteSource { diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 1fc850565de8e..15f962b430703 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -7,7 +7,6 @@ import * as os from 'os'; import * as path from 'path'; import { Command, commands, Disposable, MessageOptions, Position, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation, languages, SourceControlArtifact, ProgressLocation } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; -import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import type { CommitOptions, RemoteSourcePublisher, Remote, Branch, Ref } from './api/git'; import { ForcePushMode, GitErrorCodes, RefType, Status } from './api/git.constants'; import { Git, GitError, Repository as GitRepository, Stash, Worktree } from './git'; @@ -2943,48 +2942,6 @@ export class CommandCenter { await this._branch(repository, undefined, true); } - private async generateRandomBranchName(repository: Repository, separator: string): Promise { - const config = workspace.getConfiguration('git'); - const branchRandomNameDictionary = config.get('branchRandomName.dictionary')!; - - const dictionaries: string[][] = []; - for (const dictionary of branchRandomNameDictionary) { - if (dictionary.toLowerCase() === 'adjectives') { - dictionaries.push(adjectives); - } - if (dictionary.toLowerCase() === 'animals') { - dictionaries.push(animals); - } - if (dictionary.toLowerCase() === 'colors') { - dictionaries.push(colors); - } - if (dictionary.toLowerCase() === 'numbers') { - dictionaries.push(NumberDictionary.generate({ length: 3 })); - } - } - - if (dictionaries.length === 0) { - return ''; - } - - // 5 attempts to generate a random branch name - for (let index = 0; index < 5; index++) { - const randomName = uniqueNamesGenerator({ - dictionaries, - length: dictionaries.length, - separator - }); - - // Check for local ref conflict - const refs = await repository.getRefs({ pattern: `refs/heads/${randomName}` }); - if (refs.length === 0) { - return randomName; - } - } - - return ''; - } - private async promptForBranchName(repository: Repository, defaultName?: string, initialValue?: string): Promise { const config = workspace.getConfiguration('git'); const branchPrefix = config.get('branchPrefix')!; @@ -2998,8 +2955,7 @@ export class CommandCenter { } const getBranchName = async (): Promise => { - const branchName = branchRandomNameEnabled ? await this.generateRandomBranchName(repository, branchWhitespaceChar) : ''; - return `${branchPrefix}${branchName}`; + return await repository.generateRandomBranchName() ?? branchPrefix; }; const getValueSelection = (value: string): [number, number] | undefined => { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 6810f3cca4223..b79bb3bc4aabf 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import TelemetryReporter from '@vscode/extension-telemetry'; +import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import * as fs from 'fs'; import * as fsPromises from 'fs/promises'; import * as path from 'path'; @@ -3294,6 +3295,56 @@ export class Repository implements Disposable { return this.unpublishedCommits; } + async generateRandomBranchName(): Promise { + const config = workspace.getConfiguration('git', Uri.file(this.root)); + const branchRandomNameEnabled = config.get('branchRandomName.enable', false); + + if (!branchRandomNameEnabled) { + return undefined; + } + + const branchPrefix = config.get('branchPrefix', ''); + const branchWhitespaceChar = config.get('branchWhitespaceChar', '-'); + const branchRandomNameDictionary = config.get('branchRandomName.dictionary', ['adjectives', 'animals']); + + const dictionaries: string[][] = []; + for (const dictionary of branchRandomNameDictionary) { + if (dictionary.toLowerCase() === 'adjectives') { + dictionaries.push(adjectives); + } + if (dictionary.toLowerCase() === 'animals') { + dictionaries.push(animals); + } + if (dictionary.toLowerCase() === 'colors') { + dictionaries.push(colors); + } + if (dictionary.toLowerCase() === 'numbers') { + dictionaries.push(NumberDictionary.generate({ length: 3 })); + } + } + + if (dictionaries.length === 0) { + return undefined; + } + + // 5 attempts to generate a random branch name + for (let index = 0; index < 5; index++) { + const randomName = uniqueNamesGenerator({ + dictionaries, + length: dictionaries.length, + separator: branchWhitespaceChar + }); + + // Check for local ref conflict + const refs = await this.getRefs({ pattern: `refs/heads/${branchPrefix}${randomName}` }); + if (refs.length === 0) { + return `${branchPrefix}${randomName}`; + } + } + + return undefined; + } + dispose(): void { this.disposables = dispose(this.disposables); } diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index c6ec6ece45c69..cbf1b56e34e51 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -867,10 +867,12 @@ export function getStashDescription(stash: Stash): string | undefined { return descriptionSegments.join(' \u2022 '); } +export const CopilotWorktreeBranchPrefix = 'copilot-worktree-'; + export function isCopilotWorktree(path: string): boolean { const lastSepIndex = path.lastIndexOf(sep); return lastSepIndex !== -1 - ? path.substring(lastSepIndex + 1).startsWith('copilot-worktree-') - : path.startsWith('copilot-worktree-'); + ? path.substring(lastSepIndex + 1).startsWith(CopilotWorktreeBranchPrefix) + : path.startsWith(CopilotWorktreeBranchPrefix); } diff --git a/extensions/github/.vscodeignore b/extensions/github/.vscodeignore index a6590bd39343c..77ec048a6daff 100644 --- a/extensions/github/.vscodeignore +++ b/extensions/github/.vscodeignore @@ -2,7 +2,7 @@ src/** !src/common/config.json out/** build/** -esbuild*.mts +extension.webpack.config.js tsconfig*.json package-lock.json testWorkspace/** diff --git a/extensions/github/esbuild.mts b/extensions/github/extension.webpack.config.js similarity index 50% rename from extensions/github/esbuild.mts rename to extensions/github/extension.webpack.config.js index f91916e622d6e..9e2b191a389d4 100644 --- a/extensions/github/esbuild.mts +++ b/extensions/github/extension.webpack.config.js @@ -2,18 +2,22 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'node:path'; -import { run } from '../esbuild-extension-common.mts'; +// @ts-check +import withDefaults from '../shared.webpack.config.mjs'; -const srcDir = path.join(import.meta.dirname, 'src'); -const outDir = path.join(import.meta.dirname, 'dist'); - -run({ - platform: 'node', - format: 'esm', - entryPoints: { - 'extension': path.join(srcDir, 'extension.ts'), +export default withDefaults({ + context: import.meta.dirname, + entry: { + extension: './src/extension.ts' + }, + output: { + libraryTarget: 'module', + chunkFormat: 'module', + }, + externals: { + 'vscode': 'module vscode', }, - srcDir, - outdir: outDir, -}, process.argv); + experiments: { + outputModule: true + } +}); diff --git a/extensions/github/package.json b/extensions/github/package.json index 42f408ac96b9d..bce90fe1812d5 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -19,7 +19,8 @@ "extensionDependencies": [ "vscode.git-base" ], - "main": "./dist/extension.js", + "main": "./out/extension.js", + "type": "module", "capabilities": { "virtualWorkspaces": true, "untrustedWorkspaces": { diff --git a/extensions/github/src/branchProtection.ts b/extensions/github/src/branchProtection.ts index 0c616d33905ea..040df24942a60 100644 --- a/extensions/github/src/branchProtection.ts +++ b/extensions/github/src/branchProtection.ts @@ -6,7 +6,7 @@ import { EventEmitter, LogOutputChannel, Memento, Uri, workspace } from 'vscode'; import { Repository as GitHubRepository, RepositoryRuleset } from '@octokit/graphql-schema'; import { AuthenticationError, OctokitService } from './auth.js'; -import type { API, BranchProtection, BranchProtectionProvider, BranchProtectionRule, Repository } from './typings/git.d.ts'; +import { API, BranchProtection, BranchProtectionProvider, BranchProtectionRule, Repository } from './typings/git.js'; import { DisposableStore, getRepositoryFromUrl } from './util.js'; import { TelemetryReporter } from '@vscode/extension-telemetry'; diff --git a/extensions/github/src/canonicalUriProvider.ts b/extensions/github/src/canonicalUriProvider.ts index 9218707ed2605..0838c7377dd63 100644 --- a/extensions/github/src/canonicalUriProvider.ts +++ b/extensions/github/src/canonicalUriProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken, CanonicalUriProvider, CanonicalUriRequestOptions, Disposable, ProviderResult, Uri, workspace } from 'vscode'; -import type { API } from './typings/git.d.ts'; +import { API } from './typings/git.js'; const SUPPORTED_SCHEMES = ['ssh', 'https', 'file']; diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 4a1d1c10ce84c..33acf5a406b87 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { RefType } from './typings/git.constants.js'; -import type { API as GitAPI, Repository } from './typings/git.d.ts'; +import { API as GitAPI, RefType, Repository } from './typings/git.js'; import { publishRepository } from './publish.js'; import { DisposableStore, getRepositoryFromUrl } from './util.js'; import { LinkContext, getCommitLink, getLink, getVscodeDevHost } from './links.js'; diff --git a/extensions/github/src/credentialProvider.ts b/extensions/github/src/credentialProvider.ts index 4964724eed6b9..d184960c23bbb 100644 --- a/extensions/github/src/credentialProvider.ts +++ b/extensions/github/src/credentialProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { CredentialsProvider, Credentials, API as GitAPI } from './typings/git.d.ts'; +import { CredentialsProvider, Credentials, API as GitAPI } from './typings/git.js'; import { workspace, Uri, Disposable } from 'vscode'; import { getSession } from './auth.js'; diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index e6a44f516ac1d..17906c57d44f2 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -6,7 +6,7 @@ import { commands, Disposable, ExtensionContext, extensions, l10n, LogLevel, LogOutputChannel, window } from 'vscode'; import { TelemetryReporter } from '@vscode/extension-telemetry'; import { GithubRemoteSourceProvider } from './remoteSourceProvider.js'; -import type { API, GitExtension } from './typings/git.d.ts'; +import { API, GitExtension } from './typings/git.js'; import { registerCommands } from './commands.js'; import { GithubCredentialProviderManager } from './credentialProvider.js'; import { DisposableStore, repositoryHasGitHubRemote } from './util.js'; diff --git a/extensions/github/src/historyItemDetailsProvider.ts b/extensions/github/src/historyItemDetailsProvider.ts index d0a145ec9f23b..9a267b9e8443b 100644 --- a/extensions/github/src/historyItemDetailsProvider.ts +++ b/extensions/github/src/historyItemDetailsProvider.ts @@ -5,7 +5,7 @@ import { Command, l10n, LogOutputChannel, workspace } from 'vscode'; import { Commit, Repository as GitHubRepository, Maybe } from '@octokit/graphql-schema'; -import type { API, AvatarQuery, AvatarQueryCommit, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git.d.ts'; +import { API, AvatarQuery, AvatarQueryCommit, Repository, SourceControlHistoryItemDetailsProvider } from './typings/git.js'; import { DisposableStore, getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl, getRepositoryFromUrl, groupBy, sequentialize } from './util.js'; import { AuthenticationError, OctokitService } from './auth.js'; import { getAvatarLink } from './links.js'; diff --git a/extensions/github/src/links.ts b/extensions/github/src/links.ts index fbdde106149cd..b4f8379e5f79e 100644 --- a/extensions/github/src/links.ts +++ b/extensions/github/src/links.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { RefType } from './typings/git.constants.js'; -import type { API as GitAPI, Repository } from './typings/git.d.ts'; +import { API as GitAPI, RefType, Repository } from './typings/git.js'; import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util.js'; export function isFileInRepo(repository: Repository, file: vscode.Uri): boolean { diff --git a/extensions/github/src/publish.ts b/extensions/github/src/publish.ts index dab81037d5924..618f752745020 100644 --- a/extensions/github/src/publish.ts +++ b/extensions/github/src/publish.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import type { API as GitAPI, Repository } from './typings/git.d.ts'; +import { API as GitAPI, Repository } from './typings/git.js'; import { getOctokit } from './auth.js'; import { TextEncoder } from 'util'; import { basename } from 'path'; diff --git a/extensions/github/src/pushErrorHandler.ts b/extensions/github/src/pushErrorHandler.ts index 751654515f982..f7b0b9ef8696d 100644 --- a/extensions/github/src/pushErrorHandler.ts +++ b/extensions/github/src/pushErrorHandler.ts @@ -6,8 +6,7 @@ import { TextDecoder } from 'util'; import { commands, env, ProgressLocation, Uri, window, workspace, QuickPickOptions, FileType, l10n, Disposable, TextDocumentContentProvider } from 'vscode'; import { getOctokit } from './auth.js'; -import { GitErrorCodes } from './typings/git.constants.js'; -import type { PushErrorHandler, Remote, Repository } from './typings/git.d.ts'; +import { GitErrorCodes, PushErrorHandler, Remote, Repository } from './typings/git.js'; import * as path from 'path'; import { TelemetryReporter } from '@vscode/extension-telemetry'; diff --git a/extensions/github/src/remoteSourcePublisher.ts b/extensions/github/src/remoteSourcePublisher.ts index 67c1e567e3687..97ce05a835cf8 100644 --- a/extensions/github/src/remoteSourcePublisher.ts +++ b/extensions/github/src/remoteSourcePublisher.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { publishRepository } from './publish.js'; -import type { API as GitAPI, RemoteSourcePublisher, Repository } from './typings/git.d.ts'; +import { API as GitAPI, RemoteSourcePublisher, Repository } from './typings/git.js'; export class GithubRemoteSourcePublisher implements RemoteSourcePublisher { readonly name = 'GitHub'; diff --git a/extensions/github/src/shareProviders.ts b/extensions/github/src/shareProviders.ts index a52cf84d7044a..d2e94a471477d 100644 --- a/extensions/github/src/shareProviders.ts +++ b/extensions/github/src/shareProviders.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import type { API } from './typings/git.d.ts'; +import { API } from './typings/git.js'; import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util.js'; import { encodeURIComponentExceptSlashes, ensurePublished, getRepositoryForFile, notebookCellRangeString, rangeString } from './links.js'; diff --git a/extensions/github/src/typings/git.constants.ts b/extensions/github/src/typings/git.constants.ts deleted file mode 100644 index 5847e21d5d0da..0000000000000 --- a/extensions/github/src/typings/git.constants.ts +++ /dev/null @@ -1,98 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import type * as git from './git'; - -export type ForcePushMode = git.ForcePushMode; -export type RefType = git.RefType; -export type Status = git.Status; -export type GitErrorCodes = git.GitErrorCodes; - -export const ForcePushMode = Object.freeze({ - Force: 0, - ForceWithLease: 1, - ForceWithLeaseIfIncludes: 2, -}) satisfies typeof git.ForcePushMode; - -export const RefType = Object.freeze({ - Head: 0, - RemoteHead: 1, - Tag: 2, -}) satisfies typeof git.RefType; - -export const Status = Object.freeze({ - INDEX_MODIFIED: 0, - INDEX_ADDED: 1, - INDEX_DELETED: 2, - INDEX_RENAMED: 3, - INDEX_COPIED: 4, - - MODIFIED: 5, - DELETED: 6, - UNTRACKED: 7, - IGNORED: 8, - INTENT_TO_ADD: 9, - INTENT_TO_RENAME: 10, - TYPE_CHANGED: 11, - - ADDED_BY_US: 12, - ADDED_BY_THEM: 13, - DELETED_BY_US: 14, - DELETED_BY_THEM: 15, - BOTH_ADDED: 16, - BOTH_DELETED: 17, - BOTH_MODIFIED: 18, -}) satisfies typeof git.Status; - -export const GitErrorCodes = Object.freeze({ - BadConfigFile: 'BadConfigFile', - BadRevision: 'BadRevision', - AuthenticationFailed: 'AuthenticationFailed', - NoUserNameConfigured: 'NoUserNameConfigured', - NoUserEmailConfigured: 'NoUserEmailConfigured', - NoRemoteRepositorySpecified: 'NoRemoteRepositorySpecified', - NotAGitRepository: 'NotAGitRepository', - NotASafeGitRepository: 'NotASafeGitRepository', - NotAtRepositoryRoot: 'NotAtRepositoryRoot', - Conflict: 'Conflict', - StashConflict: 'StashConflict', - UnmergedChanges: 'UnmergedChanges', - PushRejected: 'PushRejected', - ForcePushWithLeaseRejected: 'ForcePushWithLeaseRejected', - ForcePushWithLeaseIfIncludesRejected: 'ForcePushWithLeaseIfIncludesRejected', - RemoteConnectionError: 'RemoteConnectionError', - DirtyWorkTree: 'DirtyWorkTree', - CantOpenResource: 'CantOpenResource', - GitNotFound: 'GitNotFound', - CantCreatePipe: 'CantCreatePipe', - PermissionDenied: 'PermissionDenied', - CantAccessRemote: 'CantAccessRemote', - RepositoryNotFound: 'RepositoryNotFound', - RepositoryIsLocked: 'RepositoryIsLocked', - BranchNotFullyMerged: 'BranchNotFullyMerged', - NoRemoteReference: 'NoRemoteReference', - InvalidBranchName: 'InvalidBranchName', - BranchAlreadyExists: 'BranchAlreadyExists', - NoLocalChanges: 'NoLocalChanges', - NoStashFound: 'NoStashFound', - LocalChangesOverwritten: 'LocalChangesOverwritten', - NoUpstreamBranch: 'NoUpstreamBranch', - IsInSubmodule: 'IsInSubmodule', - WrongCase: 'WrongCase', - CantLockRef: 'CantLockRef', - CantRebaseMultipleBranches: 'CantRebaseMultipleBranches', - PatchDoesNotApply: 'PatchDoesNotApply', - NoPathFound: 'NoPathFound', - UnknownPath: 'UnknownPath', - EmptyCommitMessage: 'EmptyCommitMessage', - BranchFastForwardRejected: 'BranchFastForwardRejected', - BranchNotYetBorn: 'BranchNotYetBorn', - TagConflict: 'TagConflict', - CherryPickEmpty: 'CherryPickEmpty', - CherryPickConflict: 'CherryPickConflict', - WorktreeContainsChanges: 'WorktreeContainsChanges', - WorktreeAlreadyExists: 'WorktreeAlreadyExists', - WorktreeBranchAlreadyUsed: 'WorktreeBranchAlreadyUsed', -}) satisfies Record; diff --git a/extensions/github/src/util.ts b/extensions/github/src/util.ts index bcdddaed6e5e7..2247292dd93ba 100644 --- a/extensions/github/src/util.ts +++ b/extensions/github/src/util.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import type { Repository } from './typings/git.d.ts'; +import { Repository } from './typings/git.js'; export class DisposableStore { diff --git a/extensions/go/cgmanifest.json b/extensions/go/cgmanifest.json index b697426969b2a..bdad000b91991 100644 --- a/extensions/go/cgmanifest.json +++ b/extensions/go/cgmanifest.json @@ -6,12 +6,12 @@ "git": { "name": "go-syntax", "repositoryUrl": "https://github.com/worlpaker/go-syntax", - "commitHash": "6e8421faf8f1445512825f63925e54a62106bcf1" + "commitHash": "c74e22eb9ef32958e3edd130ea750ce78d8b8241" } }, "license": "MIT", "description": "The file syntaxes/go.tmLanguage.json is from https://github.com/worlpaker/go-syntax, which in turn was derived from https://github.com/jeff-hykin/better-go-syntax.", - "version": "0.8.5" + "version": "0.8.6" } ], "version": 1 diff --git a/extensions/go/syntaxes/go.tmLanguage.json b/extensions/go/syntaxes/go.tmLanguage.json index 72d7df0cb4045..b2aec3c4e14d4 100644 --- a/extensions/go/syntaxes/go.tmLanguage.json +++ b/extensions/go/syntaxes/go.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/worlpaker/go-syntax/commit/6e8421faf8f1445512825f63925e54a62106bcf1", + "version": "https://github.com/worlpaker/go-syntax/commit/c74e22eb9ef32958e3edd130ea750ce78d8b8241", "name": "Go", "scopeName": "source.go", "patterns": [ @@ -1929,12 +1929,12 @@ }, { "comment": "one line with semicolon(;) without formatting gofmt - single type | property variables and types", - "match": "(?:(?<=\\{)((?:\\s*(?:(?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))?(?:(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[^\\s/]+)(?:\\;)?))+)\\s*(?=\\}))", + "match": "(?:(?<=\\{)((?:\\s*(?:(?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))?(?:(?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[^\\s/\\`\"]+)(?:\\;)?))+)\\s*(?=\\}))", "captures": { "1": { "patterns": [ { - "match": "(?:((?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))?((?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[^\\s/]+)(?:\\;)?))", + "match": "(?:((?:(?:\\w+\\,\\s*)+)?(?:\\w+\\s+))?((?:(?:\\s*(?:[\\*\\[\\]]+)?(?:\\<\\-\\s*)?\\bchan\\b(?:\\s*\\<\\-)?\\s*)+)?(?:[^\\s/\\`\"]+)(?:\\;)?))", "captures": { "1": { "patterns": [ diff --git a/extensions/php/cgmanifest.json b/extensions/php/cgmanifest.json index 090bdf642f9ac..1fe92816e2cce 100644 --- a/extensions/php/cgmanifest.json +++ b/extensions/php/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "language-php", "repositoryUrl": "https://github.com/KapitanOczywisty/language-php", - "commitHash": "6941b924add3b2587a5be789248176edf5f14595" + "commitHash": "a0f3d9a3b0d017181455ed515e48a36607a90e3b" } }, "license": "MIT", diff --git a/extensions/php/syntaxes/php.tmLanguage.json b/extensions/php/syntaxes/php.tmLanguage.json index efb122c98d30b..afa1a5bbb6773 100644 --- a/extensions/php/syntaxes/php.tmLanguage.json +++ b/extensions/php/syntaxes/php.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/KapitanOczywisty/language-php/commit/6941b924add3b2587a5be789248176edf5f14595", + "version": "https://github.com/KapitanOczywisty/language-php/commit/a0f3d9a3b0d017181455ed515e48a36607a90e3b", "scopeName": "source.php", "patterns": [ { @@ -2464,7 +2464,7 @@ "name": "punctuation.definition.arguments.begin.bracket.round.php" } }, - "end": "\\)", + "end": "\\)|(?=\\?>)", "endCaptures": { "0": { "name": "punctuation.definition.arguments.end.bracket.round.php" @@ -2536,16 +2536,33 @@ ] }, "invoke-call": { - "captures": { + "begin": "(?i)((\\$+)[a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)\\s*(\\()", + "beginCaptures": { "1": { "name": "variable.other.php" }, "2": { "name": "punctuation.definition.variable.php" + }, + "3": { + "name": "punctuation.definition.arguments.begin.bracket.round.php" + } + }, + "end": "\\)|(?=\\?>)", + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.end.bracket.round.php" } }, - "match": "(?i)((\\$+)[a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)(?=\\s*\\()", - "name": "meta.function-call.invoke.php" + "name": "meta.function-call.invoke.php", + "patterns": [ + { + "include": "#named-arguments" + }, + { + "include": "$self" + } + ] }, "namespace": { "begin": "(?i)(?:(namespace)|[a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)?(\\\\)", diff --git a/extensions/ruby/cgmanifest.json b/extensions/ruby/cgmanifest.json index 5d7a966206122..0fb779250dd30 100644 --- a/extensions/ruby/cgmanifest.json +++ b/extensions/ruby/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "Shopify/ruby-lsp", "repositoryUrl": "https://github.com/Shopify/ruby-lsp", - "commitHash": "59da6a0ae3409437474b85d0daa5535f1878699d" + "commitHash": "ba41f8b4f9677fb14c1ecbe15d73ebe12a0d3859" } }, "licenseDetail": [ diff --git a/extensions/ruby/syntaxes/ruby.tmLanguage.json b/extensions/ruby/syntaxes/ruby.tmLanguage.json index f5e3f2b0c0d27..8cda18871b5ce 100644 --- a/extensions/ruby/syntaxes/ruby.tmLanguage.json +++ b/extensions/ruby/syntaxes/ruby.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Shopify/ruby-lsp/commit/59da6a0ae3409437474b85d0daa5535f1878699d", + "version": "https://github.com/Shopify/ruby-lsp/commit/ba41f8b4f9677fb14c1ecbe15d73ebe12a0d3859", "name": "Ruby", "scopeName": "source.ruby", "patterns": [ @@ -1583,7 +1583,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)HTML)\\b\\1))", "comment": "Heredoc with embedded HTML", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)HTML)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.html", "patterns": [ { @@ -1594,12 +1599,7 @@ } }, "contentName": "text.html", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)HTML)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1620,7 +1620,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)HAML)\\b\\1))", "comment": "Heredoc with embedded HAML", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)HAML)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.haml", "patterns": [ { @@ -1631,12 +1636,7 @@ } }, "contentName": "text.haml", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)HAML)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1657,7 +1657,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)XML)\\b\\1))", "comment": "Heredoc with embedded XML", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)XML)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.xml", "patterns": [ { @@ -1668,12 +1673,7 @@ } }, "contentName": "text.xml", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)XML)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1694,7 +1694,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)SQL)\\b\\1))", "comment": "Heredoc with embedded SQL", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)SQL)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.sql", "patterns": [ { @@ -1705,12 +1710,7 @@ } }, "contentName": "source.sql", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)SQL)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1731,7 +1731,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:GRAPHQL|GQL))\\b\\1))", "comment": "Heredoc with embedded GraphQL", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)(?:GRAPHQL|GQL))$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.graphql", "patterns": [ { @@ -1742,12 +1747,7 @@ } }, "contentName": "source.graphql", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)(?:GRAPHQL|GQL))\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1768,7 +1768,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)CSS)\\b\\1))", "comment": "Heredoc with embedded CSS", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)CSS)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.css", "patterns": [ { @@ -1779,12 +1784,7 @@ } }, "contentName": "source.css", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)CSS)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1805,7 +1805,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)CPP)\\b\\1))", "comment": "Heredoc with embedded C++", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)CPP)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.cpp", "patterns": [ { @@ -1816,12 +1821,7 @@ } }, "contentName": "source.cpp", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)CPP)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1842,7 +1842,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)C)\\b\\1))", "comment": "Heredoc with embedded C", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)C)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.c", "patterns": [ { @@ -1853,12 +1858,7 @@ } }, "contentName": "source.c", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)C)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1879,7 +1879,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:JS|JAVASCRIPT))\\b\\1))", "comment": "Heredoc with embedded Javascript", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)(?:JS|JAVASCRIPT))$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.js", "patterns": [ { @@ -1890,12 +1895,7 @@ } }, "contentName": "source.js", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)(?:JS|JAVASCRIPT))\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1916,7 +1916,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)JQUERY)\\b\\1))", "comment": "Heredoc with embedded jQuery Javascript", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)JQUERY)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.js.jquery", "patterns": [ { @@ -1927,12 +1932,7 @@ } }, "contentName": "source.js.jquery", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)JQUERY)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1953,7 +1953,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:SH|SHELL))\\b\\1))", "comment": "Heredoc with embedded Shell", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)(?:SH|SHELL))$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.shell", "patterns": [ { @@ -1964,12 +1969,7 @@ } }, "contentName": "source.shell", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)(?:SH|SHELL))\\s*$)", "patterns": [ { "include": "#heredoc" @@ -1990,7 +1990,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)LUA)\\b\\1))", "comment": "Heredoc with embedded Lua", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)LUA)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.lua", "patterns": [ { @@ -2001,12 +2006,7 @@ } }, "contentName": "source.lua", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)LUA)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -2027,7 +2027,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)RUBY)\\b\\1))", "comment": "Heredoc with embedded Ruby", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)RUBY)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.ruby", "patterns": [ { @@ -2038,12 +2043,7 @@ } }, "contentName": "source.ruby", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)RUBY)\\s*$)", "patterns": [ { "include": "#heredoc" @@ -2064,7 +2064,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:YAML|YML))\\b\\1))", "comment": "Heredoc with embedded YAML", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)(?:YAML|YML))$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.yaml", "patterns": [ { @@ -2075,12 +2080,7 @@ } }, "contentName": "source.yaml", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)(?:YAML|YML))\\s*$)", "patterns": [ { "include": "#heredoc" @@ -2101,7 +2101,12 @@ { "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)SLIM)\\b\\1))", "comment": "Heredoc with embedded Slim", - "end": "(?!\\G)", + "end": "^\\s*((?:[_\\w]+_|)SLIM)$\\n?", + "endCaptures": { + "0": { + "name": "string.definition.end.ruby" + } + }, "name": "meta.embedded.block.slim", "patterns": [ { @@ -2112,12 +2117,7 @@ } }, "contentName": "text.slim", - "end": "^\\s*\\2$\\n?", - "endCaptures": { - "0": { - "name": "string.definition.end.ruby" - } - }, + "while": "^(?!\\s*((?:[_\\w]+_|)SLIM)\\s*$)", "patterns": [ { "include": "#heredoc" diff --git a/extensions/swift/cgmanifest.json b/extensions/swift/cgmanifest.json index ecd2705da2a90..02ea0744ecd2c 100644 --- a/extensions/swift/cgmanifest.json +++ b/extensions/swift/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "jtbandes/swift-tmlanguage", "repositoryUrl": "https://github.com/jtbandes/swift-tmlanguage", - "commitHash": "45ac01d47c6d63402570c2c36bcfbadbd1c7bca6" + "commitHash": "3fca2fa10f7dc962d19ee617b17844d6eecfa2cb" } }, "license": "MIT" diff --git a/extensions/swift/syntaxes/swift.tmLanguage.json b/extensions/swift/syntaxes/swift.tmLanguage.json index a8bbe5d00b479..d52cabb836ba4 100644 --- a/extensions/swift/syntaxes/swift.tmLanguage.json +++ b/extensions/swift/syntaxes/swift.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jtbandes/swift-tmlanguage/commit/45ac01d47c6d63402570c2c36bcfbadbd1c7bca6", + "version": "https://github.com/jtbandes/swift-tmlanguage/commit/3fca2fa10f7dc962d19ee617b17844d6eecfa2cb", "name": "Swift", "scopeName": "source.swift", "comment": "See swift.tmbundle/grammar-test.swift for test cases.", @@ -3848,7 +3848,7 @@ }, { "name": "string.quoted.double.block.raw.swift", - "begin": "#\"\"\"", + "begin": "#\"\"\"(?!#)(?=(?:[^\"]|\"(?!#))*$)", "end": "\"\"\"#(#*)", "beginCaptures": { "0": { @@ -3884,7 +3884,7 @@ }, { "name": "string.quoted.double.block.raw.swift", - "begin": "(##+)\"\"\"", + "begin": "(? .pane > .pane-header { border-top: 1px solid var(--vscode-sideBarSectionHeader-border) !important; } -/* Editor - the ::after pseudo-element draws inset shadows on each edge, - * creating the illusion that sidebar, panel, and auxiliarybar float above it. */ -.monaco-workbench.vs .part.editor { - position: relative; -} - -.monaco-workbench.vs .part.editor::after { - content: ''; - position: absolute; - inset: 0; - pointer-events: none; - z-index: 10; - box-shadow: - inset var(--shadow-depth-x), - inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.04), - inset 0 calc(-1 * 5px) 10px -4px rgba(0, 0, 0, 0.05); -} - -/* When sidebar is on the right, flip the stronger shadow to the right edge */ -.monaco-workbench.sidebar-right.vs .part.editor::after { - box-shadow: - inset 5px 0 10px -4px rgba(0, 0, 0, 0.04), - inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.05), - inset 0 calc(-1 * 5px) 10px -4px rgba(0, 0, 0, 0.05); -} - -/* Panel positions: strengthen the shadow on whichever edge faces the panel */ -.monaco-workbench.panel-position-left.vs .part.editor::after { - box-shadow: - inset var(--shadow-depth-x), - inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.04); -} - -.monaco-workbench.panel-position-right.vs .part.editor::after { - box-shadow: - inset 5px 0 10px -4px rgba(0, 0, 0, 0.04), - inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.05); -} - -.monaco-workbench.panel-position-top.vs .part.editor::after { - box-shadow: - inset var(--shadow-depth-x), - inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.04), - inset 0 var(--shadow-depth-y); -} -.monaco-workbench.vs .part.editor > .content .editor-group-container > .title { - box-shadow: none; -} - -.monaco-workbench.vs .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { - box-shadow: inset var(--shadow-active-tab); -} - -.monaco-workbench.vs .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover:not(.active) { - box-shadow: var(--shadow-sm); -} - /* Tab border bottom - make transparent */ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container { --tabs-border-bottom-color: transparent !important; @@ -110,16 +21,7 @@ --tab-border-bottom-color: transparent !important; } -/* Title Bar */ -.monaco-workbench.vs .part.titlebar { - box-shadow: var(--shadow-md); -} - /* Quick Input (Command Palette) */ -.monaco-workbench .quick-input-widget { - box-shadow: var(--shadow-xl) !important; -} - .monaco-workbench.vs-dark .quick-input-widget { border: 1px solid var(--vscode-menu-border) !important; } @@ -170,7 +72,6 @@ } .monaco-workbench .quick-input-widget .monaco-inputbox { - box-shadow: none !important; background: transparent !important; } @@ -180,10 +81,6 @@ /* Chat Widget */ -.monaco-workbench.vs .interactive-session .chat-input-container { - box-shadow: inset var(--shadow-sm); -} - .monaco-workbench .part.panel .interactive-session, .monaco-workbench .part.auxiliarybar .interactive-session { position: relative; @@ -195,97 +92,42 @@ /* Notifications */ -.monaco-workbench .notifications-toasts, -.monaco-workbench > .notifications-toasts .notification-toast-container { - overflow: visible; -} - .monaco-workbench .notifications-list-container .monaco-list-rows { background: transparent !important; } /* Context Menus */ - -.monaco-workbench .context-view .monaco-menu { - box-shadow: var(--shadow-lg); - border: none; -} - -.monaco-workbench .monaco-select-box-dropdown-container { - box-shadow: var(--shadow-lg); -} - -.monaco-workbench .monaco-menu-container > .monaco-scrollable-element { - box-shadow: var(--shadow-lg) !important; -} - .monaco-workbench .action-widget .action-widget-action-bar { background: transparent; } /* Suggest Widget */ -.monaco-workbench .monaco-editor .suggest-widget { - box-shadow: var(--shadow-lg); -} - .monaco-workbench.vs-dark .monaco-editor .suggest-widget { border: 1px solid var(--vscode-editorWidget-border); } -/* Find Widget */ -.monaco-workbench .monaco-editor .find-widget { - box-shadow: var(--shadow-lg); -} - -.monaco-workbench .inline-chat-gutter-menu { - box-shadow: var(--shadow-lg); -} - /* Dialog */ .monaco-workbench .monaco-dialog-box { border: 1px solid var(--vscode-dialog-border); - box-shadow: var(--shadow-xl); } /* Peek View */ -.monaco-workbench .monaco-editor .peekview-widget { - box-shadow: var(--shadow-hover); -} - .monaco-workbench .monaco-editor .peekview-widget .head, .monaco-workbench .monaco-editor .peekview-widget .body { background: transparent !important; } -.monaco-editor .monaco-hover { - box-shadow: var(--shadow-sm-strong); -} - -.monaco-workbench .monaco-hover.workbench-hover, -.monaco-hover.workbench-hover { - box-shadow: var(--shadow-sm-strong); -} - .monaco-workbench .defineKeybindingWidget { border: 1px solid var(--vscode-editorWidget-border); - box-shadow: var(--shadow-lg) !important; -} - -.monaco-workbench .chat-editor-overlay-widget, -.monaco-workbench .chat-diff-change-content-widget { - box-shadow: var(--shadow-md); } +/* Chat Editor Overlay */ .monaco-workbench.vs-dark .chat-editor-overlay-widget, .monaco-workbench.vs-dark .chat-diff-change-content-widget { border: 1px solid var(--vscode-editorWidget-border); } /* Settings */ -.monaco-workbench .settings-editor .settings-toc-container { - 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: transparent !important; @@ -293,27 +135,6 @@ border: 1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent) !important; } -/* Welcome Tiles */ -.monaco-workbench .part.editor .welcomePageContainer .tile { - box-shadow: var(--shadow-md); - border: none; - border-radius: var(--radius-lg); -} - -.monaco-workbench .part.editor .welcomePageContainer .tile:hover { - box-shadow: var(--shadow-hover); -} - -/* Extensions */ -.monaco-workbench .extensions-list .extension-list-item { - box-shadow: var(--shadow-sm); - border: none; -} - -.monaco-workbench .extensions-list .extension-list-item:hover { - box-shadow: var(--shadow-md); -} - /* Breadcrumbs */ .monaco-workbench.vs .breadcrumbs-control { @@ -321,8 +142,6 @@ } /* Input Boxes */ - - .monaco-inputbox .monaco-action-bar .action-item .codicon, .monaco-workbench .search-container .input-box, .monaco-custom-toggle { @@ -358,23 +177,7 @@ box-shadow: none; } -/* Dropdowns */ -.monaco-workbench .monaco-dropdown .dropdown-menu { - box-shadow: var(--shadow-lg); -} - -/* SCM */ -.monaco-workbench .scm-view .scm-provider { - box-shadow: var(--shadow-sm); -} - -/* Debug Toolbar */ -.monaco-workbench .debug-toolbar { - box-shadow: var(--shadow-lg); -} - .monaco-workbench .debug-hover-widget { - box-shadow: var(--shadow-lg); color: var(--vscode-editor-foreground) !important; } @@ -382,16 +185,6 @@ background-color: var(--vscode-list-hoverBackground); } -/* Action Widget */ -.monaco-workbench .action-widget { - box-shadow: var(--shadow-lg) !important; -} - -/* Parameter Hints */ -.monaco-workbench .monaco-editor .parameter-hints-widget { - box-shadow: var(--shadow-lg); -} - /* Minimap */ .monaco-workbench .monaco-editor .minimap canvas { @@ -400,7 +193,6 @@ .monaco-workbench.vs-dark .monaco-editor .minimap, .monaco-workbench .monaco-editor .minimap-shadow-visible { - box-shadow: var(--shadow-md); opacity: 0.85; background-color: var(--vscode-editor-background); left: 0; @@ -417,7 +209,6 @@ /* Sticky Scroll */ .monaco-workbench .monaco-editor .sticky-widget { - box-shadow: var(--shadow-md) !important; border-bottom: var(--vscode-editorWidget-border) !important; background: transparent !important; } @@ -447,32 +238,21 @@ } .monaco-editor .rename-box.preview { - box-shadow: var(--shadow-hover) !important; border: 1px solid var(--vscode-editorWidget-border); } /* Notebook */ -.monaco-workbench .notebookOverlay .monaco-list-row .cell-editor-part:before { - box-shadow: inset var(--shadow-sm); -} - .notebookOverlay .monaco-list-row .cell-title-toolbar { background-color: var(--vscode-editorWidget-background) !important; - box-shadow: var(--shadow-sm); } /* Inline Chat */ .monaco-workbench .monaco-editor .inline-chat { - box-shadow: var(--shadow-lg); border: none; } /* Command Center */ -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center { - box-shadow: inset var(--shadow-sm) !important; -} - .monaco-workbench .part.titlebar .command-center .agent-status-pill { border-color: var(--vscode-input-border); } diff --git a/package-lock.json b/package-lock.json index 00fc750db9c38..f6af524389bcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,8 +84,8 @@ "@types/yazl": "^2.4.2", "@typescript-eslint/utils": "^8.45.0", "@typescript/native-preview": "^7.0.0-dev.20260130", - "@vscode/component-explorer": "^0.1.1-16", - "@vscode/component-explorer-cli": "^0.1.1-12", + "@vscode/component-explorer": "^0.1.1-19", + "@vscode/component-explorer-cli": "^0.1.1-15", "@vscode/gulp-electron": "1.40.1", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.20.2", @@ -3051,9 +3051,9 @@ "license": "CC-BY-4.0" }, "node_modules/@vscode/component-explorer": { - "version": "0.1.1-16", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-16.tgz", - "integrity": "sha512-is1RxdlNO5K1RSqWd5z8BN6gPrqEBZfjgUi3ZJbQj8Z4VqmqoJsNLIzBXOIlQJX+5mWgeNdOq3vxe0u15ZkAlA==", + "version": "0.1.1-19", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer/-/component-explorer-0.1.1-19.tgz", + "integrity": "sha512-wvcjw1A8wSH/oR5q+lZrBSyOQZfvXtLPYkQJBj11FBKu35iHko0FTIPMG25Ee+TpT2/BWLd29dWwiJODDQbC8w==", "dev": true, "license": "MIT", "dependencies": { @@ -3062,9 +3062,9 @@ } }, "node_modules/@vscode/component-explorer-cli": { - "version": "0.1.1-12", - "resolved": "https://registry.npmjs.org/@vscode/component-explorer-cli/-/component-explorer-cli-0.1.1-12.tgz", - "integrity": "sha512-SaChUP94wkf1RaaJ/MnpQsxsr7pUpqQJq5Z9QLbrZuUqRil2TZEHwYLSqpQPqLgybNxZtrlMDivTjcCWXFTttg==", + "version": "0.1.1-15", + "resolved": "https://registry.npmjs.org/@vscode/component-explorer-cli/-/component-explorer-cli-0.1.1-15.tgz", + "integrity": "sha512-5unK3ehSezNAGJqN4Nn1CjIjavLY9Rc17buUOC/4SfqyXSFStWN/0JG7S/ESgwqW1I2WruadZis0X0sS33dlFQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8e5dc8cdf8f20..7bc8f3f17d2cb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.111.0", - "distro": "447d8d5a69fba52fcf6ea15dd29e92dd5dbbd2cd", + "distro": "e802965a9da346fb619bb708f64e54e927167133", "author": { "name": "Microsoft Corporation" }, @@ -31,6 +31,7 @@ "watch-client": "npm run gulp watch-client", "watch-clientd": "deemon npm run watch-client", "kill-watch-clientd": "deemon --kill npm run watch-client", + "transpile-client": "node build/next/index.ts transpile", "watch-client-transpile": "node build/next/index.ts transpile --watch", "watch-client-transpiled": "deemon npm run watch-client-transpile", "kill-watch-client-transpiled": "deemon --kill npm run watch-client-transpile", @@ -153,8 +154,8 @@ "@types/yazl": "^2.4.2", "@typescript-eslint/utils": "^8.45.0", "@typescript/native-preview": "^7.0.0-dev.20260130", - "@vscode/component-explorer": "^0.1.1-16", - "@vscode/component-explorer-cli": "^0.1.1-12", + "@vscode/component-explorer": "^0.1.1-19", + "@vscode/component-explorer-cli": "^0.1.1-15", "@vscode/gulp-electron": "1.40.1", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.20.2", @@ -250,4 +251,4 @@ "optionalDependencies": { "windows-foreground-love": "0.6.1" } -} +} \ No newline at end of file diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index 844b50bec385d..0dd289c9f9ee1 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -31,6 +31,7 @@ padding: 10px; transform: translate3d(0px, 0px, 0px); border-radius: var(--vscode-cornerRadius-large); + box-shadow: var(--vscode-shadow-xl); } .monaco-dialog-box.align-vertical { diff --git a/src/vs/base/browser/ui/dropdown/dropdown.css b/src/vs/base/browser/ui/dropdown/dropdown.css index 0e2d6bdd9f53c..7c70f376b1491 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.css +++ b/src/vs/base/browser/ui/dropdown/dropdown.css @@ -22,6 +22,7 @@ .monaco-dropdown .dropdown-menu { border-radius: var(--vscode-cornerRadius-large); + box-shadow: var(--vscode-shadow-lg); } .monaco-dropdown-with-primary { diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index bd6298d0485a0..0b6afe0e2eb59 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -322,13 +322,11 @@ export class Menu extends ActionBar { const bgColor = style.backgroundColor ?? ''; const border = style.borderColor ? `1px solid ${style.borderColor}` : ''; const borderRadius = 'var(--vscode-cornerRadius-large)'; - const shadow = style.shadowColor ? `0 2px 8px ${style.shadowColor}` : ''; scrollElement.style.outline = border; scrollElement.style.borderRadius = borderRadius; scrollElement.style.color = fgColor; scrollElement.style.backgroundColor = bgColor; - scrollElement.style.boxShadow = shadow; } override getContainer(): HTMLElement { @@ -1241,6 +1239,7 @@ ${formatRule(Codicon.menuSubmenu)} border: none; animation: fadeIn 0.083s linear; -webkit-app-region: no-drag; + box-shadow: var(--vscode-shadow-lg${style.shadowColor ? `, 0 0 12px ${style.shadowColor}` : ''}); } .context-view.monaco-menu-container :focus, diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.css b/src/vs/base/browser/ui/selectBox/selectBoxCustom.css index 62f4224062975..769ba3a08aa26 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.css +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.css @@ -7,7 +7,7 @@ display: none; box-sizing: border-box; border-radius: var(--vscode-cornerRadius-large); - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); } .monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown * { diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.css b/src/vs/editor/browser/viewParts/minimap/minimap.css index 35bb6e3b7178b..73814f2397990 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.css +++ b/src/vs/editor/browser/viewParts/minimap/minimap.css @@ -25,7 +25,7 @@ background: var(--vscode-minimapSlider-activeBackground); } .monaco-editor .minimap-shadow-visible { - box-shadow: var(--vscode-scrollbar-shadow) -6px 0 6px -6px inset; + box-shadow: var(--vscode-shadow-md); } .monaco-editor .minimap-shadow-hidden { position: absolute; diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.css b/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.css index 496b989268e8f..cce094ae6dc5f 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.css +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.css @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ .post-edit-widget { - box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); border: 1px solid var(--vscode-widget-border, transparent); border-radius: 4px; color: var(--vscode-button-foreground); diff --git a/src/vs/editor/contrib/find/browser/findOptionsWidget.css b/src/vs/editor/contrib/find/browser/findOptionsWidget.css index 2188fa9fa9db1..ac9f95c4e34ae 100644 --- a/src/vs/editor/contrib/find/browser/findOptionsWidget.css +++ b/src/vs/editor/contrib/find/browser/findOptionsWidget.css @@ -6,6 +6,6 @@ .monaco-editor .findOptionsWidget { background-color: var(--vscode-editorWidget-background); color: var(--vscode-editorWidget-foreground); - box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); border: 2px solid var(--vscode-contrastBorder); } diff --git a/src/vs/editor/contrib/find/browser/findWidget.css b/src/vs/editor/contrib/find/browser/findWidget.css index cbe6028775c93..42e63375f2c83 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.css +++ b/src/vs/editor/contrib/find/browser/findWidget.css @@ -15,7 +15,7 @@ margin-top: 4px; box-sizing: border-box; transform: translateY(calc(-100% - 10px)); /* shadow (10px) */ - box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); color: var(--vscode-editorWidget-foreground); border: 1px solid var(--vscode-widget-border); border-radius: var(--vscode-cornerRadius-large); diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css index 422e073e5e739..2182ed732e6df 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css @@ -7,16 +7,39 @@ padding: 2px 4px; color: var(--vscode-foreground); background-color: var(--vscode-editorWidget-background); - border-radius: 6px; + border-radius: var(--vscode-cornerRadius-medium); border: 1px solid var(--vscode-contrastBorder); display: flex; align-items: center; justify-content: center; gap: 4px; z-index: 10; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); overflow: hidden; + &.single-button { + background-color: transparent; + border-width: 0; + padding: 0; + overflow: visible; + + .action-item > .action-label, + .action-item > .action-label.codicon:not(.separator) { + height: 28px; + line-height: 28px; + border-radius: var(--vscode-cornerRadius-medium); + box-shadow: 0 2px 8px var(--vscode-widget-shadow); + } + + .action-item > .action-label { + padding: 0 8px; + } + + .action-item > .action-label.codicon:not(.separator) { + width: 28px; + } + } + .actions-container { gap: 4px; } @@ -25,7 +48,7 @@ padding: 4px 6px; font-size: 11px; line-height: 14px; - border-radius: 4px; + border-radius: var(--vscode-cornerRadius-small); } .action-item > .action-label.codicon:not(.separator) { @@ -50,3 +73,16 @@ background-color: var(--vscode-button-hoverBackground) !important; } } + +.hc-black .floating-menu-overlay-widget.single-button, +.hc-light .floating-menu-overlay-widget.single-button { + border-width: 1px; + border-style: solid; + border-color: var(--vscode-contrastBorder); + background-color: var(--vscode-editorWidget-background); + padding: 0; + .action-item > .action-label, + .action-item > .action-label.codicon:not(.separator) { + box-shadow: none; + } +} diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts index 1a530186e669f..5e7be22f374a7 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Separator } from '../../../../base/common/actions.js'; import { h } from '../../../../base/browser/dom.js'; import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { getActionBarActions, MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { autorun, constObservable, derived, IObservable, observableFromEvent } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; +import { getActionBarActions, MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -75,25 +76,27 @@ export class FloatingEditorToolbarWidget extends Disposable { const menu = this._register(menuService.createMenu(_menuId, _scopedContextKeyService)); const menuGroupsObs = observableFromEvent(this, menu.onDidChange, () => menu.getActions()); - const menuPrimaryActionIdObs = derived(reader => { + const menuPrimaryActionsObs = derived(reader => { const menuGroups = menuGroupsObs.read(reader); - const { primary } = getActionBarActions(menuGroups, () => true); - return primary.length > 0 ? primary[0].id : undefined; + return primary.filter(a => a.id !== Separator.ID); }); - this.hasActions = derived(reader => menuGroupsObs.read(reader).length > 0); + this.hasActions = derived(reader => menuPrimaryActionsObs.read(reader).length > 0); this.element = h('div.floating-menu-overlay-widget').root; this._register(toDisposable(() => this.element.remove())); - // Set height explicitly to ensure that the floating menu element - // is rendered in the lower right corner at the correct position. - this.element.style.height = '26px'; - this._register(autorun(reader => { - const hasActions = this.hasActions.read(reader); - const menuPrimaryActionId = menuPrimaryActionIdObs.read(reader); + const primaryActions = menuPrimaryActionsObs.read(reader); + const hasActions = primaryActions.length > 0; + const menuPrimaryActionId = hasActions ? primaryActions[0].id : undefined; + + const isSingleButton = primaryActions.length === 1; + this.element.classList.toggle('single-button', isSingleButton); + // Set height explicitly to ensure that the floating menu element + // is rendered in the lower right corner at the correct position. + this.element.style.height = isSingleButton ? '28px' : '26px'; if (!hasActions) { return; diff --git a/src/vs/editor/contrib/hover/browser/hover.css b/src/vs/editor/contrib/hover/browser/hover.css index b33ea5e76bce9..5817cd9a3c52f 100644 --- a/src/vs/editor/contrib/hover/browser/hover.css +++ b/src/vs/editor/contrib/hover/browser/hover.css @@ -23,6 +23,7 @@ border-radius: var(--vscode-cornerRadius-large); color: var(--vscode-editorHoverWidget-foreground); background-color: var(--vscode-editorHoverWidget-background); + box-shadow: var(--vscode-shadow-hover); } .monaco-editor .monaco-hover a { diff --git a/src/vs/editor/contrib/parameterHints/browser/parameterHints.css b/src/vs/editor/contrib/parameterHints/browser/parameterHints.css index bf54d22d60ed9..d613456e1626f 100644 --- a/src/vs/editor/contrib/parameterHints/browser/parameterHints.css +++ b/src/vs/editor/contrib/parameterHints/browser/parameterHints.css @@ -14,6 +14,7 @@ background-color: var(--vscode-editorHoverWidget-background); border: 1px solid var(--vscode-editorHoverWidget-border); border-radius: var(--vscode-cornerRadius-large); + box-shadow: var(--vscode-shadow-lg); } .hc-black .monaco-editor .parameter-hints-widget, diff --git a/src/vs/editor/contrib/peekView/browser/media/peekViewWidget.css b/src/vs/editor/contrib/peekView/browser/media/peekViewWidget.css index f49973bfea3fe..eb777d9ac7f42 100644 --- a/src/vs/editor/contrib/peekView/browser/media/peekViewWidget.css +++ b/src/vs/editor/contrib/peekView/browser/media/peekViewWidget.css @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.monaco-editor .peekview-widget { + box-shadow: var(--vscode-shadow-hover); +} + .monaco-editor .peekview-widget .head { box-sizing: border-box; display: flex; diff --git a/src/vs/editor/contrib/rename/browser/renameWidget.css b/src/vs/editor/contrib/rename/browser/renameWidget.css index acd375f2afb7e..730bf8895b8fc 100644 --- a/src/vs/editor/contrib/rename/browser/renameWidget.css +++ b/src/vs/editor/contrib/rename/browser/renameWidget.css @@ -7,6 +7,7 @@ z-index: 100; color: inherit; border-radius: 4px; + box-shadow: var(--vscode-shadow-hover); } .monaco-editor .rename-box.preview { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css index ecc59245decf8..1cd84683e709c 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css @@ -7,7 +7,7 @@ overflow: hidden; border-bottom: 1px solid var(--vscode-editorStickyScroll-border); width: 100%; - box-shadow: var(--vscode-editorStickyScroll-shadow) 0 4px 2px -2px; + box-shadow: var(--vscode-shadow-md); z-index: 4; right: initial !important; margin-left: '0px'; diff --git a/src/vs/editor/contrib/suggest/browser/media/suggest.css b/src/vs/editor/contrib/suggest/browser/media/suggest.css index 2d9fd8b1f7c01..70f27a8fa869a 100644 --- a/src/vs/editor/contrib/suggest/browser/media/suggest.css +++ b/src/vs/editor/contrib/suggest/browser/media/suggest.css @@ -11,6 +11,7 @@ display: flex; flex-direction: column; border-radius: var(--vscode-cornerRadius-large); + box-shadow: var(--vscode-shadow-lg); } .monaco-editor .suggest-widget.message { diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index 96fc6cbf50661..f6d3dc6133ffc 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -15,7 +15,7 @@ background-color: var(--vscode-menu-background); color: var(--vscode-menu-foreground); padding: 4px; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); } .context-view-block { diff --git a/src/vs/platform/hover/browser/hover.css b/src/vs/platform/hover/browser/hover.css index 597738d306964..af76fdc3702cd 100644 --- a/src/vs/platform/hover/browser/hover.css +++ b/src/vs/platform/hover/browser/hover.css @@ -17,7 +17,7 @@ border: 1px solid var(--vscode-editorHoverWidget-border); border-radius: 5px; color: var(--vscode-editorHoverWidget-foreground); - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-hover); } .monaco-hover.workbench-hover .monaco-action-bar .action-item .codicon { diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index bfad8086272b0..01831b024b921 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -10,6 +10,7 @@ left: 50%; -webkit-app-region: no-drag; border-radius: var(--vscode-cornerRadius-xLarge); + box-shadow: var(--vscode-shadow-xl); } .quick-input-titlebar { diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 8e5283ef9ad92..162d90de81b21 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -922,13 +922,12 @@ export class QuickInputController extends Disposable { private updateStyles() { if (this.ui) { const { - quickInputTitleBackground, quickInputBackground, quickInputForeground, widgetBorder, widgetShadow, + quickInputTitleBackground, quickInputBackground, quickInputForeground, widgetBorder, } = this.styles.widget; this.ui.titleBar.style.backgroundColor = quickInputTitleBackground ?? ''; this.ui.container.style.backgroundColor = quickInputBackground ?? ''; this.ui.container.style.color = quickInputForeground ?? ''; this.ui.container.style.border = widgetBorder ? `1px solid ${widgetBorder}` : ''; - this.ui.container.style.boxShadow = widgetShadow ? `0 0 8px 2px ${widgetShadow}` : ''; this.ui.list.style(this.styles.list); this.ui.tree.tree.style(this.styles.list); diff --git a/src/vs/sessions/browser/layoutActions.ts b/src/vs/sessions/browser/layoutActions.ts index cd1f588050490..c9bf983b7c963 100644 --- a/src/vs/sessions/browser/layoutActions.ts +++ b/src/vs/sessions/browser/layoutActions.ts @@ -52,7 +52,7 @@ class ToggleSidebarVisibilityAction extends Action2 { }, menu: [ { - id: Menus.TitleBarLeft, + id: Menus.TitleBarLeftLayout, group: 'navigation', order: 0, when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()) @@ -104,7 +104,7 @@ class ToggleSecondarySidebarVisibilityAction extends Action2 { f1: true, menu: [ { - id: Menus.TitleBarRight, + id: Menus.TitleBarRightLayout, group: 'navigation', order: 10, when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()) @@ -165,7 +165,7 @@ registerAction2(ToggleSecondarySidebarVisibilityAction); registerAction2(TogglePanelVisibilityAction); // Floating window controls: always-on-top -MenuRegistry.appendMenuItem(Menus.TitleBarRight, { +MenuRegistry.appendMenuItem(Menus.TitleBarRightLayout, { command: { id: 'workbench.action.toggleWindowAlwaysOnTop', title: localize('toggleWindowAlwaysOnTop', "Toggle Always on Top"), diff --git a/src/vs/sessions/browser/menus.ts b/src/vs/sessions/browser/menus.ts index 74ebe982e7d2a..c322fba968b6b 100644 --- a/src/vs/sessions/browser/menus.ts +++ b/src/vs/sessions/browser/menus.ts @@ -13,11 +13,10 @@ export const Menus = { CommandCenter: new MenuId('SessionsCommandCenter'), CommandCenterCenter: new MenuId('SessionsCommandCenterCenter'), TitleBarContext: new MenuId('SessionsTitleBarContext'), - TitleBarControlMenu: new MenuId('SessionsTitleBarControlMenu'), - TitleBarLeft: new MenuId('SessionsTitleBarLeft'), - TitleBarCenter: new MenuId('SessionsTitleBarCenter'), - TitleBarRight: new MenuId('SessionsTitleBarRight'), - OpenSubMenu: new MenuId('SessionsOpenSubMenu'), + TitleBarLeftLayout: new MenuId('SessionsTitleBarLeftLayout'), + TitleBarSessionTitle: new MenuId('SessionsTitleBarSessionTitle'), + TitleBarSessionMenu: new MenuId('SessionsTitleBarSessionMenu'), + TitleBarRightLayout: new MenuId('SessionsTitleBarRightLayout'), PanelTitle: new MenuId('SessionsPanelTitle'), SidebarTitle: new MenuId('SessionsSidebarTitle'), AuxiliaryBarTitle: new MenuId('SessionsAuxiliaryBarTitle'), @@ -25,5 +24,4 @@ export const Menus = { SidebarFooter: new MenuId('SessionsSidebarFooter'), SidebarCustomizations: new MenuId('SessionsSidebarCustomizations'), AgentFeedbackEditorContent: new MenuId('AgentFeedbackEditorContent'), - SessionTitleActions: new MenuId('SessionTitleActions'), } as const; diff --git a/src/vs/sessions/browser/parts/media/titlebarpart.css b/src/vs/sessions/browser/parts/media/titlebarpart.css index 6aab26d26318c..146ed67fa619d 100644 --- a/src/vs/sessions/browser/parts/media/titlebarpart.css +++ b/src/vs/sessions/browser/parts/media/titlebarpart.css @@ -8,10 +8,74 @@ height: 100%; align-items: center; order: 0; - flex-grow: 2; + flex-grow: 0; + flex-shrink: 0; + width: auto; + justify-content: flex-start; +} + +.monaco-workbench .part.titlebar > .sessions-titlebar-container.has-center > .titlebar-center { + order: 1; + width: auto; + flex-grow: 0; + flex-shrink: 1; + min-width: 0px; + margin: 0; justify-content: flex-start; } +.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left { + width: fit-content; + flex-grow: 0; +} + +.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-center { + flex: 1; + max-width: none; +} + +.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-center .window-title { + margin: unset; +} + +.agent-sessions-workbench.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-right { + order: 2; + width: fit-content; + flex-grow: 0; + justify-content: flex-end; + margin-right: 10px; +} + +/* Session Title Actions Container (before right toolbar) */ +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-right > .titlebar-actions-container { + display: none; + flex-shrink: 0; + -webkit-app-region: no-drag; + height: 100%; +} + +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-right > .titlebar-actions-container:not(.has-no-actions) { + display: flex; + align-items: center; +} + +/* Separator between session actions and layout actions toolbar */ +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-right > .titlebar-session-actions-container:not(.has-no-actions) + .titlebar-layout-actions-container:not(.has-no-actions)::before { + content: ''; + width: 1px; + height: 16px; + margin: 0 4px; + background-color: var(--vscode-disabledForeground); +} + +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-right > .titlebar-actions-container .codicon { + color: inherit; +} + +.monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-right > .titlebar-actions-container .monaco-action-bar .action-item { + display: flex; +} + /* Left Tool Bar Container */ .monaco-workbench .part.titlebar > .sessions-titlebar-container > .titlebar-left > .left-toolbar-container { display: none; @@ -40,11 +104,8 @@ display: flex; } -/* TODO: Hack to avoid flicker when sidebar becomes visible. - * The contribution swaps the menu item synchronously, but the toolbar - * re-render is async, causing a brief flash. Hide the container via - * CSS when sidebar is visible (nosidebar class is removed synchronously). */ -.agent-sessions-workbench:not(.nosidebar) .part.titlebar > .sessions-titlebar-container > .titlebar-left > .left-toolbar-container { +/* Hide the entire titlebar left when the sidebar is visible */ +.agent-sessions-workbench:not(.nosidebar) .part.titlebar > .sessions-titlebar-container > .titlebar-left { display: none !important; } @@ -52,7 +113,3 @@ .agent-sessions-workbench.mac .part.titlebar .window-controls-container { -webkit-app-region: drag; } - -.agent-sessions-workbench:not(.nosidebar) .part.titlebar > .sessions-titlebar-container > .titlebar-left .window-controls-container { - display: none !important; -} diff --git a/src/vs/sessions/browser/parts/sidebarPart.ts b/src/vs/sessions/browser/parts/sidebarPart.ts index b9132d4d13e63..75eb74869ddc4 100644 --- a/src/vs/sessions/browser/parts/sidebarPart.ts +++ b/src/vs/sessions/browser/parts/sidebarPart.ts @@ -120,7 +120,7 @@ export class SidebarPart extends AbstractPaneCompositePart { ViewContainerLocation.Sidebar, Extensions.Viewlets, Menus.SidebarTitle, - Menus.TitleBarLeft, + Menus.TitleBarLeftLayout, notificationService, storageService, contextMenuService, diff --git a/src/vs/sessions/browser/parts/titlebarPart.ts b/src/vs/sessions/browser/parts/titlebarPart.ts index fa99c2e21e3eb..18c2d2867f08d 100644 --- a/src/vs/sessions/browser/parts/titlebarPart.ts +++ b/src/vs/sessions/browser/parts/titlebarPart.ts @@ -185,7 +185,7 @@ export class TitlebarPart extends Part implements ITitlebarPart { // Left toolbar (driven by Menus.TitleBarLeft, rendered after window controls via CSS order) const leftToolbarContainer = append(this.leftContent, $('div.left-toolbar-container')); - this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, leftToolbarContainer, Menus.TitleBarLeft, { + this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, leftToolbarContainer, Menus.TitleBarLeftLayout, { contextMenu: Menus.TitleBarContext, telemetrySource: 'titlePart.left', hiddenItemStrategy: HiddenItemStrategy.NoHide, @@ -204,13 +204,22 @@ export class TitlebarPart extends Part implements ITitlebarPart { })); // Right toolbar (driven by Menus.TitleBarRight - includes account submenu) - const rightToolbarContainer = prepend(this.rightContent, $('div.action-toolbar-container')); - this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, rightToolbarContainer, Menus.TitleBarRight, { + const rightToolbarContainer = prepend(this.rightContent, $('div.titlebar-actions-container.titlebar-layout-actions-container')); + this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, rightToolbarContainer, Menus.TitleBarRightLayout, { contextMenu: Menus.TitleBarContext, telemetrySource: 'titlePart.right', toolbarOptions: { primaryGroup: () => true }, })); + // Session title actions toolbar (before right toolbar) + const sessionActionsContainer = prepend(this.rightContent, $('div.titlebar-actions-container.titlebar-session-actions-container')); + this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, sessionActionsContainer, Menus.TitleBarSessionMenu, { + contextMenu: Menus.TitleBarContext, + hiddenItemStrategy: HiddenItemStrategy.NoHide, + telemetrySource: 'titlePart.sessionActions', + toolbarOptions: { primaryGroup: () => true }, + })); + // Context menu on the titlebar this._register(addDisposableListener(this.rootContainer, EventType.CONTEXT_MENU, e => { EventHelper.stop(e); diff --git a/src/vs/sessions/contrib/accountMenu/test/browser/updateWidget.fixture.ts b/src/vs/sessions/contrib/accountMenu/test/browser/updateWidget.fixture.ts index a67edb78b11b7..225223a3dda42 100644 --- a/src/vs/sessions/contrib/accountMenu/test/browser/updateWidget.fixture.ts +++ b/src/vs/sessions/contrib/accountMenu/test/browser/updateWidget.fixture.ts @@ -56,48 +56,59 @@ function renderUpdateWidget(ctx: ComponentFixtureContext, state: State): void { widget.render(ctx.container); } -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'sessions/' }, { Ready: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (ctx) => renderUpdateWidget(ctx, State.Ready(mockUpdate, true, false)), }), CheckingForUpdates: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.CheckingForUpdates(true)), }), AvailableForDownload: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.AvailableForDownload(mockUpdate)), }), Downloading0Percent: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 0, 100_000_000)), }), Downloading30Percent: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 30_000_000, 100_000_000)), }), Downloading65Percent: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 65_000_000, 100_000_000)), }), Downloading100Percent: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false, 100_000_000, 100_000_000)), }), DownloadingIndeterminate: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Downloading(mockUpdate, true, false)), }), Downloaded: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Downloaded(mockUpdate, true, false)), }), Updating: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Updating(mockUpdate)), }), Overwriting: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderUpdateWidget(ctx, State.Overwriting(mockUpdate, true)), }), }); diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css index f2361debfc603..b467ff7f7aa8d 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorInput.css @@ -8,7 +8,7 @@ z-index: 10000; background-color: var(--vscode-panel-background); border: 1px solid var(--vscode-agentFeedbackInputWidget-border, var(--vscode-input-border, var(--vscode-widget-border))); - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); border-radius: 8px; padding: 4px; display: flex; diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css index 1acdbe228ce56..0de61925c9eb9 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorOverlay.css @@ -14,7 +14,7 @@ justify-content: center; gap: 4px; z-index: 10; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); overflow: hidden; } diff --git a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css index 3ca674d2cf444..34725117ebedf 100644 --- a/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css +++ b/src/vs/sessions/contrib/agentFeedback/browser/media/agentFeedbackEditorWidget.css @@ -11,7 +11,7 @@ background-color: var(--vscode-editorWidget-background); border: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder)); border-radius: 8px; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); font-size: 12px; line-height: 1.4; opacity: 0; diff --git a/src/vs/sessions/contrib/changesView/browser/changesViewActions.ts b/src/vs/sessions/contrib/changesView/browser/changesViewActions.ts index 1a06ef09fb512..9ca4eeb503dcf 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesViewActions.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesViewActions.ts @@ -3,28 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/changesViewActions.css'; -import { $, reset } from '../../../../base/browser/dom.js'; -import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { autorun, observableFromEvent } from '../../../../base/common/observable.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { localize, localize2 } from '../../../../nls.js'; -import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { observableFromEvent } from '../../../../base/common/observable.js'; +import { localize2 } from '../../../../nls.js'; import { Action2, IAction2Options, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; -import { getAgentChangesSummary, hasValidDiff } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { hasValidDiff } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; -import { Menus } from '../../../browser/menus.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { CHANGES_VIEW_ID } from './changesView.js'; -import { IAction } from '../../../../base/common/actions.js'; -import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; import { activeSessionHasChangesContextKey } from '../common/changes.js'; @@ -34,11 +25,6 @@ const openChangesViewActionOptions: IAction2Options = { title: localize2('openChangesView', "Changes"), icon: Codicon.diffMultiple, f1: false, - menu: { - id: Menus.SessionTitleActions, - order: 1, - when: ContextKeyExpr.equals(activeSessionHasChangesContextKey.key, true), - }, }; class OpenChangesViewAction extends Action2 { @@ -57,111 +43,17 @@ class OpenChangesViewAction extends Action2 { registerAction2(OpenChangesViewAction); -/** - * Custom action view item that renders the changes summary as: - * [diff-icon] +insertions -deletions - */ -class ChangesActionViewItem extends BaseActionViewItem { - - private _container: HTMLElement | undefined; - private readonly _renderDisposables = this._register(new DisposableStore()); - - constructor( - action: IAction, - options: IBaseActionViewItemOptions | undefined, - @ISessionsManagementService private readonly sessionManagementService: ISessionsManagementService, - @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, - @IHoverService private readonly hoverService: IHoverService, - ) { - super(undefined, action, options); - - this._register(autorun(reader => { - this.sessionManagementService.activeSession.read(reader); - this._updateLabel(); - })); - - this._register(this.agentSessionsService.model.onDidChangeSessions(() => { - this._updateLabel(); - })); - } - - override render(container: HTMLElement): void { - super.render(container); - this._container = container; - container.classList.add('changes-action-view-item'); - this._updateLabel(); - } - - private _updateLabel(): void { - if (!this._container) { - return; - } - - this._renderDisposables.clear(); - reset(this._container); - - const activeSession = this.sessionManagementService.getActiveSession(); - if (!activeSession) { - this._container.style.display = 'none'; - return; - } - - const agentSession = this.agentSessionsService.getSession(activeSession.resource); - const changes = agentSession?.changes; - - if (!changes || !hasValidDiff(changes)) { - this._container.style.display = 'none'; - return; - } - - const summary = getAgentChangesSummary(changes); - if (!summary) { - this._container.style.display = 'none'; - return; - } - - this._container.style.display = ''; - - // Diff icon - const iconEl = $('span.changes-action-icon' + ThemeIcon.asCSSSelector(Codicon.diffMultiple)); - this._container.appendChild(iconEl); - - // Insertions - const addedEl = $('span.changes-action-added'); - addedEl.textContent = `+${summary.insertions}`; - this._container.appendChild(addedEl); - - // Deletions - const removedEl = $('span.changes-action-removed'); - removedEl.textContent = `-${summary.deletions}`; - this._container.appendChild(removedEl); - - // Hover - this._renderDisposables.add(this.hoverService.setupManagedHover( - getDefaultHoverDelegate('mouse'), - this._container, - localize('agentSessions.viewChanges', "View All Changes") - )); - } -} - class ChangesViewActionsContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.changesViewActions'; constructor( - @IActionViewItemService actionViewItemService: IActionViewItemService, - @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService contextKeyService: IContextKeyService, @ISessionsManagementService sessionManagementService: ISessionsManagementService, @IAgentSessionsService agentSessionsService: IAgentSessionsService, ) { super(); - this._register(actionViewItemService.register(Menus.SessionTitleActions, OpenChangesViewAction.ID, (action, options) => { - return instantiationService.createInstance(ChangesActionViewItem, action, options); - })); - // Bind context key: true when the active session has changes const sessionsChanged = observableFromEvent(this, agentSessionsService.model.onDidChangeSessions, () => { }); this._register(bindContextKey(activeSessionHasChangesContextKey, contextKeyService, reader => { diff --git a/src/vs/sessions/contrib/chat/browser/branchPicker.ts b/src/vs/sessions/contrib/chat/browser/branchPicker.ts index e12427b28ca57..b5ada806bc22f 100644 --- a/src/vs/sessions/contrib/chat/browser/branchPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/branchPicker.ts @@ -31,6 +31,7 @@ interface IBranchItem { export class BranchPicker extends Disposable { private _selectedBranch: string | undefined; + private _preferredBranch: string | undefined; private _newSession: INewSession | undefined; private _branches: string[] = []; @@ -48,6 +49,13 @@ export class BranchPicker extends Disposable { return this._selectedBranch; } + /** + * Sets a preferred branch to select when branches are loaded. + */ + setPreferredBranch(branch: string | undefined): void { + this._preferredBranch = branch; + } + constructor( @IActionWidgetService private readonly actionWidgetService: IActionWidgetService, ) { @@ -85,8 +93,11 @@ export class BranchPicker extends Disposable { .filter((name): name is string => !!name) .filter(name => !name.includes(COPILOT_WORKTREE_PATTERN)); - // Select active branch, main, master, or the first branch by default - const defaultBranch = this._branches.find(b => b === repository.state.get().HEAD?.name) + // Select preferred branch (from draft), active branch, main, master, or the first branch + const preferred = this._preferredBranch; + this._preferredBranch = undefined; + const defaultBranch = (preferred ? this._branches.find(b => b === preferred) : undefined) + ?? this._branches.find(b => b === repository.state.get().HEAD?.name) ?? this._branches.find(b => b === 'main') ?? this._branches.find(b => b === 'master') ?? this._branches[0]; diff --git a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts index 74b536c694779..c10d4b4cdc021 100644 --- a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts @@ -47,11 +47,11 @@ export class OpenSessionWorktreeInVSCodeAction extends Action2 { id: OpenSessionWorktreeInVSCodeAction.ID, title: localize2('openInVSCode', 'Open in VS Code'), icon: Codicon.vscodeInsiders, + precondition: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext), menu: [{ - id: Menus.TitleBarRight, + id: Menus.TitleBarSessionMenu, group: 'navigation', order: 10, - when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext) }] }); } @@ -92,29 +92,6 @@ export class OpenSessionWorktreeInVSCodeAction extends Action2 { } registerAction2(OpenSessionWorktreeInVSCodeAction); -// Disabled placeholder shown in the titlebar when the active session does not support opening in VS Code -class OpenSessionWorktreeInVSCodeNotAvailableAction extends Action2 { - constructor() { - super({ - id: 'chat.openSessionWorktreeInVSCode.notAvailable', - title: localize2('openInVSCode', 'Open in VS Code'), - tooltip: localize('openInVSCodeNotAvailableTooltip', "Open in VS Code is not available for this session type"), - icon: Codicon.vscodeInsiders, - precondition: ContextKeyExpr.false(), - menu: [{ - id: Menus.TitleBarRight, - group: 'navigation', - order: 10, - when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext.toNegated()) - }] - }); - } - - override run(): void { } -} - -registerAction2(OpenSessionWorktreeInVSCodeNotAvailableAction); - class NewChatInSessionsWindowAction extends Action2 { constructor() { diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 247501dfc64e9..9f285ef4c4825 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -11,7 +11,7 @@ import { toAction } from '../../../../base/common/actions.js'; import { Emitter } from '../../../../base/common/event.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { observableValue } from '../../../../base/common/observable.js'; +import { autorun, observableValue } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; @@ -56,7 +56,7 @@ import { NewChatContextAttachments } from './newChatContextAttachments.js'; import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; import { FolderPicker } from './folderPicker.js'; import { IGitService } from '../../../../workbench/contrib/git/common/gitService.js'; -import { IsolationModePicker, SessionTargetPicker } from './sessionTargetPicker.js'; +import { IsolationMode, IsolationModePicker, SessionTargetPicker } from './sessionTargetPicker.js'; import { BranchPicker } from './branchPicker.js'; import { SyncIndicator } from './syncIndicator.js'; import { INewSession, ISessionOptionGroup, RemoteNewSession } from './newSession.js'; @@ -75,6 +75,14 @@ const STORAGE_KEY_DRAFT_STATE = 'sessions.draftState'; const MIN_EDITOR_HEIGHT = 50; const MAX_EDITOR_HEIGHT = 200; +interface IDraftState extends IChatModelInputState { + target?: AgentSessionProviders; + isolationMode?: IsolationMode; + branch?: string; + folderUri?: string; + repo?: string; +} + // #region --- Chat Welcome Widget --- /** @@ -152,6 +160,16 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { // Slash commands private _slashCommandHandler: SlashCommandHandler | undefined; + // Input state + private _draftState: IDraftState | undefined = { + inputText: '', + attachments: [], + mode: { id: ChatModeKind.Agent, kind: ChatModeKind.Agent }, + selectedModel: undefined, + selections: [], + contrib: {} + }; + // Input history private readonly _history: ChatHistoryNavigator; private _historyNavigationBackwardsEnablement!: IHistoryNavigationContext['historyNavigationBackwardsEnablement']; @@ -190,6 +208,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._isolationModePicker.setVisible(isLocal); this._branchPicker.setVisible(isLocal); this._syncIndicator.setVisible(isLocal); + this._updateDraftState(); this._focusEditor(); })); @@ -200,21 +219,37 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._register(this._branchPicker.onDidChange((branch) => { this._syncIndicator.setBranch(branch); + this._updateDraftState(); this._focusEditor(); })); this._register(this._folderPicker.onDidSelectFolder(() => { + this._updateDraftState(); this._focusEditor(); })); - this._register(this._isolationModePicker.onDidChange(() => { + this._register(this._isolationModePicker.onDidChange((mode) => { + this._branchPicker.setVisible(mode === 'worktree'); + this._syncIndicator.setVisible(mode === 'worktree'); + this._updateDraftState(); this._focusEditor(); })); + this._register(this._repoPicker.onDidSelectRepo(() => { + this._updateDraftState(); + })); + // When language models change (e.g., extension activates), reinitialize if no model selected this._register(this.languageModelsService.onDidChangeLanguageModels(() => { this._initDefaultModel(); })); + + // Update input state when attachments or model change + this._register(this._contextAttachments.onDidChangeContext(() => this._updateDraftState())); + this._register(autorun(reader => { + this._currentLanguageModel.read(reader); + this._updateDraftState(); + })); } // --- Rendering --- @@ -261,11 +296,12 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._branchPicker.render(branchContainer); this._syncIndicator.render(branchContainer); - // Set initial visibility based on default target + // Set initial visibility based on default target and isolation mode const isLocal = this._targetPicker.selectedTarget === AgentSessionProviders.Background; + const isWorktree = this._isolationModePicker.isolationMode === 'worktree'; this._isolationModePicker.setVisible(isLocal); - this._branchPicker.setVisible(isLocal); - this._syncIndicator.setVisible(isLocal); + this._branchPicker.setVisible(isLocal && isWorktree); + this._syncIndicator.setVisible(isLocal && isWorktree); // Render target buttons & extension pickers this._renderOptionGroupPickers(); @@ -517,6 +553,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._slashCommandHandler = this._register(this.instantiationService.createInstance(SlashCommandHandler, this._editor)); this._register(this._editor.onDidChangeModelContent(() => { + this._updateDraftState(); this._updateSendButtonState(); })); } @@ -848,9 +885,8 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { if (this._history.isAtStart()) { return; } - const state = this._getInputState(); - if (state.inputText || state.attachments.length) { - this._history.overlay(state); + if (this._draftState?.inputText || this._draftState?.attachments.length) { + this._history.overlay(this._draftState); } this._navigateHistory(true); } @@ -859,21 +895,26 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { if (this._history.isAtEnd()) { return; } - const state = this._getInputState(); - if (state.inputText || state.attachments.length) { - this._history.overlay(state); + if (this._draftState?.inputText || this._draftState?.attachments.length) { + this._history.overlay(this._draftState); } this._navigateHistory(false); } - private _getInputState(): IChatModelInputState { - return { + private _updateDraftState(): void { + const attachments = [...this._contextAttachments.attachments]; + this._draftState = { inputText: this._editor?.getModel()?.getValue() ?? '', - attachments: [...this._contextAttachments.attachments], + attachments, mode: { id: ChatModeKind.Agent, kind: ChatModeKind.Agent }, selectedModel: this._currentLanguageModel.get(), selections: this._editor?.getSelections() ?? [], contrib: {}, + target: this._targetPicker.selectedTarget, + isolationMode: this._isolationModePicker.isolationMode, + branch: this._branchPicker.selectedBranch, + folderUri: this._folderPicker.selectedFolderUri?.toString(), + repo: this._repoPicker.selectedRepo, }; } @@ -932,7 +973,9 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._contextAttachments.attachments.length > 0 ? [...this._contextAttachments.attachments] : undefined ); - this._history.append(this._getInputState()); + if (this._draftState) { + this._history.append(this._draftState); + } this._clearDraftState(); this._sending = true; @@ -940,22 +983,23 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._updateSendButtonState(); this._updateInputLoadingState(); - this.sessionsManagementService.sendRequestForNewSession( - session.resource, - options?.openNewAfterSend ? { openNewSessionView: true } : undefined - ).then(() => { - // Release ref without disposing - the service owns disposal - this._newSession.clearAndLeak(); + + try { + await this.sessionsManagementService.sendRequestForNewSession( + session.resource, + options?.openNewAfterSend ? { openNewSessionView: true } : undefined + ); this._newSessionListener.clear(); this._contextAttachments.clear(); - }, e => { + } catch (e) { this.logService.error('Failed to send request:', e); - }).finally(() => { - this._sending = false; - this._editor.updateOptions({ readOnly: false }); - this._updateSendButtonState(); - this._updateInputLoadingState(); - }); + } + + + this._sending = false; + this._editor.updateOptions({ readOnly: false }); + this._updateSendButtonState(); + this._updateInputLoadingState(); } /** @@ -1001,10 +1045,23 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._currentLanguageModel.set(model, undefined); } } + if (draft.isolationMode) { + this._isolationModePicker.setPreferredIsolationMode(draft.isolationMode); + this._isolationModePicker.setIsolationMode(draft.isolationMode); + } + if (draft.branch) { + this._branchPicker.setPreferredBranch(draft.branch); + } + if (draft.folderUri) { + try { this._folderPicker.setSelectedFolder(URI.parse(draft.folderUri)); } catch { /* ignore */ } + } + if (draft.repo) { + this._repoPicker.setSelectedRepo(draft.repo); + } } } - private _getDraftState(): (IChatModelInputState & { target?: AgentSessionProviders }) | undefined { + private _getDraftState(): IDraftState | undefined { const raw = this.storageService.get(STORAGE_KEY_DRAFT_STATE, StorageScope.WORKSPACE); if (!raw) { return undefined; @@ -1017,21 +1074,20 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { } private _clearDraftState(): void { + this._draftState = undefined; this.storageService.remove(STORAGE_KEY_DRAFT_STATE, StorageScope.WORKSPACE); } saveState(): void { - const inputState = this._getInputState(); - const state = { - ...inputState, - attachments: inputState.attachments.map(IChatRequestVariableEntry.toExport), - target: this._targetPicker.selectedTarget, - }; - this.storageService.store(STORAGE_KEY_DRAFT_STATE, JSON.stringify(state), StorageScope.WORKSPACE, StorageTarget.MACHINE); + if (this._draftState) { + const state = { + ...this._draftState, + attachments: this._draftState.attachments.map(IChatRequestVariableEntry.toExport), + }; + this.storageService.store(STORAGE_KEY_DRAFT_STATE, JSON.stringify(state), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } } - // --- Layout --- - layout(_height: number, _width: number): void { this._editor?.layout(); } @@ -1118,6 +1174,11 @@ export class NewChatViewPane extends ViewPane { override saveState(): void { this._widget?.saveState(); } + + override dispose(): void { + this._widget?.saveState(); + super.dispose(); + } } // #endregion diff --git a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts index e5c024cb2ba0d..a1cd803058a6c 100644 --- a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts +++ b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts @@ -5,11 +5,14 @@ import { equals } from '../../../../base/common/arrays.js'; import { Codicon } from '../../../../base/common/codicons.js'; +import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { autorun, derivedOpts, IObservable } from '../../../../base/common/observable.js'; import { localize, localize2 } from '../../../../nls.js'; import { MenuId, registerAction2, Action2, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; import { SessionsCategories } from '../../../common/categories.js'; @@ -26,6 +29,7 @@ export const RunScriptDropdownMenuId = MenuId.for('AgentSessionsRunScriptDropdow // Action IDs const RUN_SCRIPT_ACTION_ID = 'workbench.action.agentSessions.runScript'; +const RUN_SCRIPT_ACTION_PRIMARY_ID = 'workbench.action.agentSessions.runScriptPrimary'; const CONFIGURE_DEFAULT_RUN_ACTION_ID = 'workbench.action.agentSessions.configureDefaultRunAction'; function getTaskDisplayLabel(task: ITaskEntry): string { @@ -62,6 +66,7 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr constructor( @ISessionsManagementService private readonly _activeSessionService: ISessionsManagementService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @ISessionsConfigurationService private readonly _sessionsConfigService: ISessionsConfigurationService, ) { @@ -94,6 +99,40 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr private _registerActions(): void { const that = this; + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: RUN_SCRIPT_ACTION_PRIMARY_ID, + title: { value: localize('runPrimaryScript', 'Run Primary Script'), original: 'Run Primary Script' }, + icon: Codicon.play, + category: SessionsCategories.Sessions, + f1: true, + }); + } + + async run(): Promise { + const activeState = that._activeRunState.get(); + if (!activeState) { + return; + } + + const { tasks, session, lastRunTaskLabel } = activeState; + if (tasks.length === 0) { + const task = await that._showConfigureQuickPick(session); + if (task) { + await that._sessionsConfigService.runTask(task, session); + } + return; + } + + const mruIndex = lastRunTaskLabel !== undefined + ? tasks.findIndex(t => t.label === lastRunTaskLabel) + : -1; + const primaryTask = tasks[mruIndex >= 0 ? mruIndex : 0]; + await that._sessionsConfigService.runTask(primaryTask, session); + } + })); + this._register(autorun(reader => { const activeState = this._activeRunState.read(reader); if (!activeState) { @@ -112,13 +151,15 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr for (let i = 0; i < tasks.length; i++) { const task = tasks[i]; const actionId = `${RUN_SCRIPT_ACTION_ID}.${i}`; + const isPrimary = i === (mruIndex >= 0 ? mruIndex : 0); reader.store.add(registerAction2(class extends Action2 { constructor() { super({ id: actionId, title: getTaskDisplayLabel(task), - tooltip: localize('runActionTooltip', "Run '{0}' in terminal", getTaskDisplayLabel(task)), + tooltip: !isPrimary ? localize('runActionTooltip', "Run '{0}' in terminal", getTaskDisplayLabel(task)) + : localize('runActionTooltipKeybinding', "Run '{0}' in terminal ({1})", getTaskDisplayLabel(task), that._keybindingService.lookupKeybinding(RUN_SCRIPT_ACTION_PRIMARY_ID)?.getLabel() ?? ''), icon: Codicon.play, category: SessionsCategories.Sessions, menu: [{ @@ -154,18 +195,20 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr } async run(): Promise { - await that._showConfigureQuickPick(session); + const task = await that._showConfigureQuickPick(session); + if (task) { + await that._sessionsConfigService.runTask(task, session); + } } })); })); } - private async _showConfigureQuickPick(session: IActiveSessionItem): Promise { + private async _showConfigureQuickPick(session: IActiveSessionItem): Promise { const nonSessionTasks = await this._sessionsConfigService.getNonSessionTasks(session); if (nonSessionTasks.length === 0) { // No existing tasks, go straight to custom command input - await this._showCustomCommandInput(session); - return; + return this._showCustomCommandInput(session); } interface ITaskPickItem extends IQuickPickItem { @@ -198,38 +241,36 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr }); if (!picked) { - return; + return undefined; } const pickedItem = picked as ITaskPickItem; if (pickedItem.task) { // Existing task — set inSessions: true await this._sessionsConfigService.addTaskToSessions(pickedItem.task, session, pickedItem.source ?? 'workspace'); + return pickedItem.task; } else { // Custom command path - await this._showCustomCommandInput(session); + return this._showCustomCommandInput(session); } } - private async _showCustomCommandInput(session: IActiveSessionItem): Promise { + private async _showCustomCommandInput(session: IActiveSessionItem): Promise { const command = await this._quickInputService.input({ placeHolder: localize('enterCommandPlaceholder', "Enter command (e.g., npm run dev)"), prompt: localize('enterCommandPrompt', "This command will be run as a task in the integrated terminal") }); if (!command) { - return; + return undefined; } const target = await this._pickStorageTarget(session); if (!target) { - return; + return undefined; } - const newTask = await this._sessionsConfigService.createAndAddTask(command, session, target); - if (newTask) { - await this._sessionsConfigService.runTask(newTask, session); - } + return this._sessionsConfigService.createAndAddTask(command, session, target); } private async _pickStorageTarget(session: IActiveSessionItem): Promise { @@ -285,7 +326,7 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr } // Register the Run split button submenu on the workbench title bar (background sessions only) -MenuRegistry.appendMenuItem(Menus.TitleBarRight, { +MenuRegistry.appendMenuItem(Menus.TitleBarSessionMenu, { submenu: RunScriptDropdownMenuId, isSplitButton: true, title: localize2('run', "Run"), @@ -305,7 +346,7 @@ class RunScriptNotAvailableAction extends Action2 { icon: Codicon.play, precondition: ContextKeyExpr.false(), menu: [{ - id: Menus.TitleBarRight, + id: Menus.TitleBarSessionMenu, group: 'navigation', order: 8, when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext.toNegated()) @@ -317,3 +358,13 @@ class RunScriptNotAvailableAction extends Action2 { } registerAction2(RunScriptNotAvailableAction); + +// Register F5 keybinding at module level to ensure it's in the registry +// before the keybinding resolver is cached. The command handler is +// registered later by RunScriptContribution. +KeybindingsRegistry.registerKeybindingRule({ + id: RUN_SCRIPT_ACTION_PRIMARY_ID, + primary: KeyCode.F5, + weight: KeybindingWeight.WorkbenchContrib + 100, + when: IsAuxiliaryWindowContext.toNegated() +}); diff --git a/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts b/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts index 9b3de3cff0589..88aaa11a7a0f3 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionTargetPicker.ts @@ -137,6 +137,7 @@ export type IsolationMode = 'worktree' | 'workspace'; export class IsolationModePicker extends Disposable { private _isolationMode: IsolationMode = 'worktree'; + private _preferredIsolationMode: IsolationMode | undefined; private _newSession: INewSession | undefined; private _repository: IGitRepository | undefined; @@ -171,7 +172,9 @@ export class IsolationModePicker extends Disposable { setRepository(repository: IGitRepository | undefined): void { this._repository = repository; if (repository) { - this._setMode('worktree'); + const preferred = this._preferredIsolationMode; + this._preferredIsolationMode = undefined; + this._setMode(preferred ?? 'worktree'); } else if (this._isolationMode === 'worktree') { this._setMode('workspace'); } @@ -207,6 +210,20 @@ export class IsolationModePicker extends Disposable { })); } + /** + * Sets a preferred isolation mode to apply when a repository is set. + */ + setPreferredIsolationMode(mode: IsolationMode): void { + this._preferredIsolationMode = mode; + } + + /** + * Programmatically set the isolation mode. + */ + setIsolationMode(mode: IsolationMode): void { + this._setMode(mode); + } + /** * Shows or hides the picker. */ diff --git a/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts b/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts index f1a859d4aada1..a36bed73da5a9 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionsConfigurationService.ts @@ -30,10 +30,11 @@ export interface ITaskEntry { readonly script?: string; readonly type?: string; readonly command?: string; + readonly args?: CommandString[]; readonly inSessions?: boolean; - readonly windows?: { command?: string }; - readonly osx?: { command?: string }; - readonly linux?: { command?: string }; + readonly windows?: { command?: string; args?: CommandString[] }; + readonly osx?: { command?: string; args?: CommandString[] }; + readonly linux?: { command?: string; args?: CommandString[] }; readonly [key: string]: unknown; } @@ -293,21 +294,42 @@ export class SessionsConfigurationService extends Disposable implements ISession if (!task.script) { return undefined; } - if (task.path) { - return `npm --prefix ${task.path} run ${task.script}`; - } - return `npm run ${task.script}`; + const base = task.path + ? `npm --prefix ${task.path} run ${task.script}` + : `npm run ${task.script}`; + return this._appendArgs(base, task.args); } + + let command: string | undefined; + let platformArgs: CommandString[] | undefined; + if (isWindows && task.windows?.command) { - return task.windows.command; + command = task.windows.command; + platformArgs = task.windows.args; + } else if (isMacintosh && task.osx?.command) { + command = task.osx.command; + platformArgs = task.osx.args; + } else if (!isWindows && !isMacintosh && task.linux?.command) { + command = task.linux.command; + platformArgs = task.linux.args; + } else { + command = task.command; } - if (isMacintosh && task.osx?.command) { - return task.osx.command; + + // Platform-specific args override task-level args + const args = platformArgs ?? task.args; + return this._appendArgs(command, args); + } + + private _appendArgs(command: string | undefined, args: CommandString[] | undefined): string | undefined { + if (!command) { + return undefined; } - if (!isWindows && !isMacintosh && task.linux?.command) { - return task.linux.command; + if (!args || args.length === 0) { + return command; } - return task.command; + const resolvedArgs = args.map(a => CommandString.value(a)).join(' '); + return `${command} ${resolvedArgs}`; } private _ensureFileWatch(folder: URI): void { diff --git a/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts b/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts index f1f2706807acd..d127cff33e906 100644 --- a/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts +++ b/src/vs/sessions/contrib/chat/test/browser/sessionsConfigurationService.test.ts @@ -396,6 +396,54 @@ suite('SessionsConfigurationService', () => { assert.strictEqual(createdTerminals[1].name, 'test'); }); + test('runTask appends args to shell command', async () => { + const session = makeSession({ worktree: worktreeUri, repository: repoUri }); + const task: ITaskEntry = { label: 'build', type: 'shell', command: 'dotnet', args: ['build', '--configuration', 'Release'], inSessions: true }; + + await service.runTask(task, session); + + assert.strictEqual(sentCommands.length, 1); + assert.strictEqual(sentCommands[0].command, 'dotnet build --configuration Release'); + }); + + test('runTask appends args to npm task', async () => { + const session = makeSession({ worktree: worktreeUri, repository: repoUri }); + const task: ITaskEntry = { label: 'test', type: 'npm', script: 'test', args: ['--', '--coverage'], inSessions: true }; + + await service.runTask(task, session); + + assert.strictEqual(sentCommands.length, 1); + assert.strictEqual(sentCommands[0].command, 'npm run test -- --coverage'); + }); + + test('runTask resolves CommandString objects in args', async () => { + const session = makeSession({ worktree: worktreeUri, repository: repoUri }); + const task: ITaskEntry = { + label: 'build', type: 'shell', command: 'gcc', + args: [ + { value: '-o', quoting: 'escape' as const }, + 'output.exe', + 'main.c', + ], + inSessions: true + }; + + await service.runTask(task, session); + + assert.strictEqual(sentCommands.length, 1); + assert.strictEqual(sentCommands[0].command, 'gcc -o output.exe main.c'); + }); + + test('runTask sends only command when args is empty', async () => { + const session = makeSession({ worktree: worktreeUri, repository: repoUri }); + const task: ITaskEntry = { label: 'build', type: 'shell', command: 'make', args: [], inSessions: true }; + + await service.runTask(task, session); + + assert.strictEqual(sentCommands.length, 1); + assert.strictEqual(sentCommands[0].command, 'make'); + }); + test('runTask creates different terminals for same command in different worktrees', async () => { const wt1 = URI.parse('file:///worktree1'); const wt2 = URI.parse('file:///worktree2'); diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index d533049d110f2..aad4b93413754 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -14,6 +14,7 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'chat.viewSessions.enabled': false, 'chat.implicitContext.suggestedContext': false, 'chat.implicitContext.enabled': { 'panel': 'never' }, + 'chat.tools.terminal.enableAutoApprove': true, 'breadcrumbs.enabled': false, diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css index 577c5a482f621..b9f14273091fa 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsTitleBarWidget.css @@ -6,13 +6,11 @@ /* Container - button style hover */ .command-center .agent-sessions-titlebar-container { display: flex; - width: 38vw; - max-width: 600px; - display: flex; + width: 100%; flex-direction: row; flex-wrap: nowrap; align-items: center; - justify-content: center; + justify-content: flex-start; padding: 0 10px; height: 22px; border-radius: 4px; @@ -30,28 +28,13 @@ padding: 0 4px; border-radius: 4px; min-width: 0; + max-width: 600px; } .command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-pill:hover { background-color: var(--vscode-toolbar-hoverBackground); } -/* Session title actions toolbar */ -.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-actions { - display: flex; - align-items: center; - flex-shrink: 0; -} - -.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-actions .actions-container { - height: auto; -} - -.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-actions .action-item { - display: flex; - align-items: center; -} - .command-center .agent-sessions-titlebar-container:focus { outline: 1px solid var(--vscode-focusBorder); outline-offset: -1px; @@ -63,11 +46,9 @@ align-items: center; gap: 6px; min-width: 0; - justify-content: center; + justify-content: flex-start; cursor: pointer; } - -/* Kind icon */ .command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-icon { display: flex; align-items: center; @@ -95,3 +76,19 @@ opacity: 0.5; flex-shrink: 0; } + +/* Changes summary */ +.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-changes { + display: flex; + align-items: center; + flex-shrink: 0; + gap: 3px; +} + +.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-changes-added { + color: var(--vscode-gitDecoration-addedResourceForeground); +} + +.command-center .agent-sessions-titlebar-container .agent-sessions-titlebar-changes-removed { + color: var(--vscode-gitDecoration-deletedResourceForeground); +} diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts index 5590f70761a8d..f15bed0dc9466 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts @@ -4,17 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import './media/sessionsTitleBarWidget.css'; -import { $, addDisposableListener, EventType, reset } from '../../../../base/browser/dom.js'; +import { $, addDisposableListener, EventType, getActiveWindow, reset } from '../../../../base/browser/dom.js'; +import { Separator } from '../../../../base/common/actions.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { MarshalledId } from '../../../../base/common/marshallingIds.js'; +import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; import { localize } from '../../../../nls.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { MenuRegistry, SubmenuItemAction } from '../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { IMenuService, MenuId, MenuRegistry, SubmenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { IContextKeyService, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; +import { IMarshalledAgentSessionContext, getAgentChangesSummary, hasValidDiff } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { Menus } from '../../../browser/menus.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; @@ -65,6 +71,10 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, @IChatService private readonly chatService: IChatService, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, ) { super(undefined, action, options); @@ -117,9 +127,10 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { const label = this._getActiveSessionLabel(); const icon = this._getActiveSessionIcon(); const repoLabel = this._getRepositoryLabel(); + const changesSummary = this._getChangesSummary(); // Build a render-state key from all displayed data - const renderState = `${icon?.id ?? ''}|${label}|${repoLabel ?? ''}`; + const renderState = `${icon?.id ?? ''}|${label}|${repoLabel ?? ''}|${changesSummary?.insertions ?? ''}|${changesSummary?.deletions ?? ''}`; // Skip re-render if state hasn't changed if (this._lastRenderState === renderState) { @@ -164,6 +175,25 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { centerGroup.appendChild(repoEl); } + // Changes summary shown next to the repo + if (changesSummary) { + const separator2 = $('span.agent-sessions-titlebar-separator'); + separator2.textContent = '\u00B7'; + centerGroup.appendChild(separator2); + + const changesEl = $('span.agent-sessions-titlebar-changes'); + + const addedEl = $('span.agent-sessions-titlebar-changes-added'); + addedEl.textContent = `+${changesSummary.insertions}`; + changesEl.appendChild(addedEl); + + const removedEl = $('span.agent-sessions-titlebar-changes-removed'); + removedEl.textContent = `-${changesSummary.deletions}`; + changesEl.appendChild(removedEl); + + centerGroup.appendChild(changesEl); + } + sessionPill.appendChild(centerGroup); // Click handler on pill - show sessions picker @@ -176,17 +206,14 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { e.stopPropagation(); this._showSessionsPicker(); })); + this._dynamicDisposables.add(addDisposableListener(sessionPill, EventType.CONTEXT_MENU, (e) => { + e.preventDefault(); + e.stopPropagation(); + this._showContextMenu(e); + })); this._container.appendChild(sessionPill); - // Session title actions toolbar (rendered next to the session title) - const actionsContainer = $('span.agent-sessions-titlebar-actions'); - this._dynamicDisposables.add(this.instantiationService.createInstance(MenuWorkbenchToolBar, actionsContainer, Menus.SessionTitleActions, { - hiddenItemStrategy: HiddenItemStrategy.NoHide, - toolbarOptions: { primaryGroup: () => true }, - })); - this._container.appendChild(actionsContainer); - // Hover this._dynamicDisposables.add(this.hoverService.setupManagedHover( getDefaultHoverDelegate('mouse'), @@ -233,20 +260,19 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { /** * Get the label of the active chat session. - * Prefers the live model title over the snapshot label from the active session service. - * Falls back to a generic label if no active session is found. */ private _getActiveSessionLabel(): string { const activeSession = this.activeSessionService.getActiveSession(); - if (activeSession?.resource) { - const model = this.chatService.getSession(activeSession.resource); - if (model?.title) { - return model.title; - } + const label = activeSession?.label; + if (label) { + return label; // prefer session label to support renamed sessions } - if (activeSession?.label) { - return activeSession.label; + if (activeSession) { + const activeModel = this.chatService.getSession(activeSession.resource); + if (activeModel?.title) { + return activeModel.title; // fall back to chat model title if available + } } return localize('agentSessions.newSession', "New Session"); @@ -293,6 +319,60 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { return basename(uri); } + private _showContextMenu(e: MouseEvent): void { + const activeSession = this.activeSessionService.getActiveSession(); + if (!activeSession) { + return; + } + + const agentSession = this.agentSessionsService.getSession(activeSession.resource); + if (!agentSession) { + return; + } + + this.chatSessionsService.activateChatSessionItemProvider(agentSession.providerType); + + const contextOverlay: Array<[string, boolean | string]> = [ + [ChatContextKeys.isArchivedAgentSession.key, agentSession.isArchived()], + [ChatContextKeys.isReadAgentSession.key, agentSession.isRead()], + [ChatContextKeys.agentSessionType.key, agentSession.providerType], + ]; + + const menu = this.menuService.createMenu(MenuId.AgentSessionsContext, this.contextKeyService.createOverlay(contextOverlay)); + + const marshalledContext: IMarshalledAgentSessionContext = { + session: agentSession, + sessions: [agentSession], + $mid: MarshalledId.AgentSessionContext, + }; + + this.contextMenuService.showContextMenu({ + getActions: () => Separator.join(...menu.getActions({ arg: marshalledContext, shouldForwardArgs: true }).map(([, actions]) => actions)), + getAnchor: () => new StandardMouseEvent(getActiveWindow(), e), + getActionsContext: () => marshalledContext + }); + + menu.dispose(); + } + + /** + * Get the changes summary for the active session. + */ + private _getChangesSummary(): { insertions: number; deletions: number } | undefined { + const activeSession = this.activeSessionService.getActiveSession(); + if (!activeSession) { + return undefined; + } + + const agentSession = this.agentSessionsService.getSession(activeSession.resource); + const changes = agentSession?.changes; + if (!changes || !hasValidDiff(changes)) { + return undefined; + } + + return getAgentChangesSummary(changes); + } + private _showSessionsPicker(): void { const picker = this.instantiationService.createInstance(AgentSessionsPicker, undefined, { overrideSessionOpen: (session, openOptions) => this.activeSessionService.openSession(session.resource, openOptions) @@ -318,14 +398,14 @@ export class SessionsTitleBarContribution extends Disposable implements IWorkben // Register the submenu item in the Agent Sessions command center this._register(MenuRegistry.appendMenuItem(Menus.CommandCenter, { - submenu: Menus.TitleBarControlMenu, + submenu: Menus.TitleBarSessionTitle, title: localize('agentSessionsControl', "Agent Sessions"), order: 101, when: ContextKeyExpr.and(IsAuxiliaryWindowContext.negate(), SessionsWelcomeVisibleContext.negate()) })); // Register a placeholder action so the submenu appears - this._register(MenuRegistry.appendMenuItem(Menus.TitleBarControlMenu, { + this._register(MenuRegistry.appendMenuItem(Menus.TitleBarSessionTitle, { command: { id: FocusAgentSessionsAction.id, title: localize('showSessions', "Show Sessions"), @@ -335,7 +415,7 @@ export class SessionsTitleBarContribution extends Disposable implements IWorkben when: IsAuxiliaryWindowContext.negate() })); - this._register(actionViewItemService.register(Menus.CommandCenter, Menus.TitleBarControlMenu, (action, options) => { + this._register(actionViewItemService.register(Menus.CommandCenter, Menus.TitleBarSessionTitle, (action, options) => { if (!(action instanceof SubmenuItemAction)) { return undefined; } diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index d4c0b5dbfc7fe..c158a65cb6a27 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -143,7 +143,9 @@ export class AgenticSessionsViewPane extends ViewPane { source: 'agentSessionsViewPane', filter: sessionsFilter, overrideStyles: this.getLocationBasedColors().listOverrideStyles, - disableHover: true, + useSimpleHover: true, + showIsolationIcon: true, + enableApprovalRow: true, getHoverPosition: () => this.getSessionHoverPosition(), trackActiveEditorSession: () => true, collapseOlderSections: () => true, diff --git a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts index 269d3a63a23bd..ff375376d0057 100644 --- a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts +++ b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts @@ -44,9 +44,10 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben static readonly ID = 'workbench.contrib.sessionsTerminal'; - /** Maps worktree/repository fsPath (lower-cased) to the terminal instance id. */ - private readonly _pathToInstanceId = new Map(); - private _lastTargetFsPath: string | undefined; + /** Maps worktree/repository fsPath (lower-cased) to terminal instance ids. */ + private readonly _pathToInstanceIds = new Map>(); + private _activeKey: string | undefined; + private _isCreatingTerminal = false; constructor( @ISessionsManagementService private readonly _sessionsManagementService: ISessionsManagementService, @@ -75,13 +76,24 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben // Clean up mapping when terminals are disposed this._register(this._terminalService.onDidDisposeInstance(instance => { - for (const [path, id] of this._pathToInstanceId) { - if (id === instance.instanceId) { - this._pathToInstanceId.delete(path); - break; + for (const [path, ids] of this._pathToInstanceIds) { + if (ids.delete(instance.instanceId) && ids.size === 0) { + this._pathToInstanceIds.delete(path); } } })); + + // When terminals are restored on startup, ensure visibility matches active session + this._register(this._terminalService.onDidCreateInstance(instance => { + if (this._isCreatingTerminal || this._activeKey === undefined) { + return; + } + // If this instance is not tracked by us, hide it + const activeIds = this._pathToInstanceIds.get(this._activeKey); + if (!activeIds?.has(instance.instanceId)) { + this._terminalService.moveToBackground(instance); + } + })); } /** @@ -91,16 +103,23 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben */ async ensureTerminal(cwd: URI, focus: boolean): Promise { const key = cwd.fsPath.toLowerCase(); - const existingId = this._pathToInstanceId.get(key); + const ids = this._pathToInstanceIds.get(key); + const existingId = ids ? ids.values().next().value : undefined; const existing = existingId !== undefined ? this._terminalService.getInstanceFromId(existingId) : undefined; if (existing) { + await this._terminalService.showBackgroundTerminal(existing); this._terminalService.setActiveInstance(existing); } else { - const instance = await this._terminalService.createTerminal({ config: { cwd } }); - this._pathToInstanceId.set(key, instance.instanceId); - this._terminalService.setActiveInstance(instance); - this._logService.trace(`[SessionsTerminal] Created terminal ${instance.instanceId} for ${cwd.fsPath}`); + this._isCreatingTerminal = true; + try { + const instance = await this._terminalService.createTerminal({ config: { cwd } }); + this._addInstanceToPath(key, instance.instanceId); + this._terminalService.setActiveInstance(instance); + this._logService.trace(`[SessionsTerminal] Created terminal ${instance.instanceId} for ${cwd.fsPath}`); + } finally { + this._isCreatingTerminal = false; + } } if (focus) { @@ -116,25 +135,68 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben const sessionCwd = getSessionCwd(session); const targetPath = sessionCwd ?? await this._pathService.userHome(); - const targetFsPath = targetPath.fsPath; - if (this._lastTargetFsPath?.toLowerCase() === targetFsPath.toLowerCase()) { + const targetKey = targetPath.fsPath.toLowerCase(); + if (this._activeKey === targetKey) { return; } - this._lastTargetFsPath = targetFsPath; + this._activeKey = targetKey; await this.ensureTerminal(targetPath, false); + + // If the active key changed while we were awaiting, a newer call has + // taken over — skip the visibility update to avoid flicker. + if (this._activeKey !== targetKey) { + return; + } + this._updateTerminalVisibility(targetKey); + } + + private _addInstanceToPath(key: string, instanceId: number): void { + let ids = this._pathToInstanceIds.get(key); + if (!ids) { + ids = new Set(); + this._pathToInstanceIds.set(key, ids); + } + ids.add(instanceId); + } + + /** + * Hides all foreground terminals that do not belong to the given active key + * and shows all background terminals that do belong to it. + */ + private _updateTerminalVisibility(activeKey: string): void { + const activeIds = this._pathToInstanceIds.get(activeKey); + + // Hide foreground terminals not belonging to the active session + for (const instance of [...this._terminalService.foregroundInstances]) { + if (!activeIds?.has(instance.instanceId)) { + this._terminalService.moveToBackground(instance); + } + } + + // Show background terminals belonging to the active session + if (activeIds) { + for (const id of activeIds) { + const instance = this._terminalService.getInstanceFromId(id); + if (instance && !this._terminalService.foregroundInstances.includes(instance)) { + this._terminalService.showBackgroundTerminal(instance, true); + } + } + } } private _closeTerminalsForPath(fsPath: string): void { const key = fsPath.toLowerCase(); - const instanceId = this._pathToInstanceId.get(key); - if (instanceId !== undefined) { - const instance = this._terminalService.getInstanceFromId(instanceId); - if (instance) { - this._terminalService.safeDisposeTerminal(instance); - this._logService.trace(`[SessionsTerminal] Closed archived terminal ${instanceId}`); + const ids = this._pathToInstanceIds.get(key); + if (ids) { + for (const instanceId of ids) { + const instance = this._terminalService.getInstanceFromId(instanceId); + if (instance) { + this._terminalService.safeDisposeTerminal(instance); + this._logService.trace(`[SessionsTerminal] Closed archived terminal ${instanceId}`); + } } - this._pathToInstanceId.delete(key); + this._pathToInstanceIds.delete(key); } } } @@ -149,9 +211,9 @@ class OpenSessionInTerminalAction extends Action2 { title: localize2('openInTerminal', "Open Terminal"), icon: Codicon.terminal, menu: [{ - id: Menus.TitleBarRight, + id: Menus.TitleBarSessionMenu, group: 'navigation', - order: 11, + order: 9, when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()) }] }); diff --git a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts index 5cb061bb85e5c..c2108711f8013 100644 --- a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts +++ b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts @@ -59,12 +59,16 @@ suite('SessionsTerminalContribution', () => { let onDidChangeSessionArchivedState: Emitter; let onDidDisposeInstance: Emitter; + let onDidCreateInstance: Emitter; let createdTerminals: { cwd: URI }[]; let activeInstanceSet: number[]; let focusCalls: number; let disposedInstances: ITerminalInstance[]; let nextInstanceId: number; let terminalInstances: Map; + let backgroundedInstances: Set; + let moveToBackgroundCalls: number[]; + let showBackgroundCalls: number[]; setup(() => { createdTerminals = []; @@ -73,12 +77,16 @@ suite('SessionsTerminalContribution', () => { disposedInstances = []; nextInstanceId = 1; terminalInstances = new Map(); + backgroundedInstances = new Set(); + moveToBackgroundCalls = []; + showBackgroundCalls = []; const instantiationService = store.add(new TestInstantiationService()); activeSessionObs = observableValue('activeSession', undefined); onDidChangeSessionArchivedState = store.add(new Emitter()); onDidDisposeInstance = store.add(new Emitter()); + onDidCreateInstance = store.add(new Emitter()); instantiationService.stub(ILogService, new NullLogService()); @@ -88,11 +96,16 @@ suite('SessionsTerminalContribution', () => { instantiationService.stub(ITerminalService, new class extends mock() { override onDidDisposeInstance = onDidDisposeInstance.event; + override onDidCreateInstance = onDidCreateInstance.event; + override get foregroundInstances(): readonly ITerminalInstance[] { + return [...terminalInstances.values()].filter(i => !backgroundedInstances.has(i.instanceId)); + } override async createTerminal(opts?: any): Promise { const id = nextInstanceId++; const instance = { instanceId: id } as ITerminalInstance; createdTerminals.push({ cwd: opts?.config?.cwd }); terminalInstances.set(id, instance); + onDidCreateInstance.fire(instance); return instance; } override getInstanceFromId(id: number): ITerminalInstance | undefined { @@ -107,6 +120,15 @@ suite('SessionsTerminalContribution', () => { override async safeDisposeTerminal(instance: ITerminalInstance): Promise { disposedInstances.push(instance); terminalInstances.delete(instance.instanceId); + backgroundedInstances.delete(instance.instanceId); + } + override moveToBackground(instance: ITerminalInstance): void { + backgroundedInstances.add(instance.instanceId); + moveToBackgroundCalls.push(instance.instanceId); + } + override async showBackgroundTerminal(instance: ITerminalInstance): Promise { + backgroundedInstances.delete(instance.instanceId); + showBackgroundCalls.push(instance.instanceId); } }); @@ -346,6 +368,105 @@ suite('SessionsTerminalContribution', () => { await tick(); assert.strictEqual(createdTerminals.length, 2, 'should reuse the terminal for cwd1'); }); + + // --- Terminal visibility management --- + + test('hides terminals from previous session when switching to a new session', async () => { + const cwd1 = URI.file('/cwd1'); + const cwd2 = URI.file('/cwd2'); + + activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + const firstTerminalId = createdTerminals.length; + assert.strictEqual(firstTerminalId, 1); + + activeSessionObs.set(makeAgentSession({ worktree: cwd2, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + // The first terminal (id=1) should have been moved to background + assert.ok(moveToBackgroundCalls.includes(1), 'terminal for cwd1 should be backgrounded'); + assert.ok(backgroundedInstances.has(1), 'terminal for cwd1 should remain backgrounded'); + }); + + test('shows previously hidden terminals when switching back to their session', async () => { + const cwd1 = URI.file('/cwd1'); + const cwd2 = URI.file('/cwd2'); + + activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + activeSessionObs.set(makeAgentSession({ worktree: cwd2, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + // Switch back to cwd1 + activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + // Terminal for cwd1 (id=1) should be shown again + assert.ok(showBackgroundCalls.includes(1), 'terminal for cwd1 should be shown'); + assert.ok(!backgroundedInstances.has(1), 'terminal for cwd1 should be foreground'); + // Terminal for cwd2 (id=2) should now be backgrounded + assert.ok(backgroundedInstances.has(2), 'terminal for cwd2 should be backgrounded'); + }); + + test('only terminals of the active session are visible after multiple switches', async () => { + const cwd1 = URI.file('/cwd1'); + const cwd2 = URI.file('/cwd2'); + const cwd3 = URI.file('/cwd3'); + + activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + activeSessionObs.set(makeAgentSession({ worktree: cwd2, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + activeSessionObs.set(makeAgentSession({ worktree: cwd3, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + // Only terminal for cwd3 (id=3) should be foreground + assert.ok(backgroundedInstances.has(1), 'terminal for cwd1 should be backgrounded'); + assert.ok(backgroundedInstances.has(2), 'terminal for cwd2 should be backgrounded'); + assert.ok(!backgroundedInstances.has(3), 'terminal for cwd3 should be foreground'); + }); + + test('hides restored terminals that do not belong to the active session', async () => { + // Set an active session first + const cwd1 = URI.file('/cwd1'); + activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Background }), undefined); + await tick(); + + // Simulate a terminal being restored (e.g. on startup) that is not tracked + const restoredInstance = { instanceId: nextInstanceId++ } as ITerminalInstance; + terminalInstances.set(restoredInstance.instanceId, restoredInstance); + onDidCreateInstance.fire(restoredInstance); + + // The restored terminal should be moved to background + assert.ok(moveToBackgroundCalls.includes(restoredInstance.instanceId), 'restored terminal should be backgrounded'); + }); + + test('does not hide restored terminals before any session is active', async () => { + // Simulate a terminal being restored before any session is active + const restoredInstance = { instanceId: nextInstanceId++ } as ITerminalInstance; + terminalInstances.set(restoredInstance.instanceId, restoredInstance); + onDidCreateInstance.fire(restoredInstance); + + assert.strictEqual(moveToBackgroundCalls.length, 0, 'should not background before any session is active'); + }); + + test('ensureTerminal shows a backgrounded terminal instead of creating a new one', async () => { + const cwd = URI.file('/test-cwd'); + await contribution.ensureTerminal(cwd, false); + const instanceId = activeInstanceSet[0]; + + // Manually background it + backgroundedInstances.add(instanceId); + + // ensureTerminal should show it, not create a new one + await contribution.ensureTerminal(cwd, false); + + assert.strictEqual(createdTerminals.length, 1, 'should not create a new terminal'); + assert.ok(showBackgroundCalls.includes(instanceId), 'should show the backgrounded terminal'); + }); }); function tick(): Promise { diff --git a/src/vs/sessions/test/browser/layoutActions.test.ts b/src/vs/sessions/test/browser/layoutActions.test.ts index 786236ec9703a..f8362f7d6549b 100644 --- a/src/vs/sessions/test/browser/layoutActions.test.ts +++ b/src/vs/sessions/test/browser/layoutActions.test.ts @@ -16,7 +16,7 @@ suite('Sessions - Layout Actions', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('always-on-top toggle action is contributed to TitleBarRight', () => { - const items = MenuRegistry.getMenuItems(Menus.TitleBarRight); + const items = MenuRegistry.getMenuItems(Menus.TitleBarRightLayout); const menuItems = items.filter(isIMenuItem); const toggleAlwaysOnTop = menuItems.find(item => item.command.id === 'workbench.action.toggleWindowAlwaysOnTop'); diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index 7537f9316e05e..0d6a2da153bfa 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -53,6 +53,18 @@ body { z-index: 1; overflow: hidden; color: var(--vscode-foreground); + + /* Elevation shadows */ + --vscode-shadow-sm: 0 0 4px rgba(0, 0, 0, 0.08); + --vscode-shadow-md: 0 0 6px rgba(0, 0, 0, 0.08); + --vscode-shadow-lg: 0 0 12px rgba(0, 0, 0, 0.14); + --vscode-shadow-xl: 0 0 20px rgba(0, 0, 0, 0.15); + --vscode-shadow-hover: 0 0 8px rgba(0, 0, 0, 0.12); + --vscode-shadow-active-tab: 0 8px 12px rgba(0, 0, 0, 0.02); + + /* Panel depth shadows cast onto the editor surface */ + --vscode-shadow-depth-x: 5px 0 10px -4px rgba(0, 0, 0, 0.05); + --vscode-shadow-depth-y: 0 5px 10px -4px rgba(0, 0, 0, 0.04); } .monaco-workbench.web { diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css index d903883d10a85..568a721298088 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css @@ -8,6 +8,15 @@ height: 100%; } +/* Activity Bar - shadow when sidebar is hidden or on the right */ +.monaco-workbench.nosidebar .part.activitybar { + box-shadow: var(--vscode-shadow-md); +} + +.monaco-workbench.activitybar-right .part.activitybar { + box-shadow: var(--vscode-shadow-md); +} + .monaco-workbench .activitybar.bordered::before { content: ''; float: left; diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts index 198f156d1969c..db602f7d37a5e 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts @@ -17,7 +17,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { FileKind, FileSystemProviderCapabilities, IFileService, IFileStat } from '../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { WorkbenchDataTree, WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js'; -import { breadcrumbsPickerBackground, widgetBorder, widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; +import { breadcrumbsPickerBackground, widgetBorder } from '../../../../platform/theme/common/colorRegistry.js'; import { isWorkspace, isWorkspaceFolder, IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js'; import { ResourceLabels, IResourceLabel, DEFAULT_LABELS_CONTAINER } from '../../labels.js'; import { BreadcrumbsConfig } from './breadcrumbs.js'; @@ -96,7 +96,7 @@ export abstract class BreadcrumbsPicker { this._treeContainer.style.background = color ? color.toString() : ''; this._treeContainer.style.paddingTop = '2px'; this._treeContainer.style.borderRadius = '3px'; - this._treeContainer.style.boxShadow = `0 0 8px 2px ${this._themeService.getColorTheme().getColor(widgetShadow)}`; + this._treeContainer.style.boxShadow = 'var(--vscode-shadow-lg)'; this._treeContainer.style.border = `1px solid ${this._themeService.getColorTheme().getColor(widgetBorder)}`; this._domNode.appendChild(this._treeContainer); diff --git a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css index 8eff5df9389e9..9f63f8390de56 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css +++ b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css @@ -5,6 +5,52 @@ /* Container */ +/* Editor depth shadows - the ::after pseudo-element draws inset shadows on each edge, + * creating the illusion that sidebar, panel, and auxiliarybar float above it. */ +.monaco-workbench.vs .part.editor { + position: relative; +} + +.monaco-workbench.vs .part.editor::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + z-index: 10; + box-shadow: + inset var(--vscode-shadow-depth-x), + inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.04), + inset 0 calc(-1 * 5px) 10px -4px rgba(0, 0, 0, 0.05); +} + +/* When sidebar is on the right, flip the stronger shadow to the right edge */ +.monaco-workbench.sidebar-right.vs .part.editor::after { + box-shadow: + inset 5px 0 10px -4px rgba(0, 0, 0, 0.04), + inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.05), + inset 0 calc(-1 * 5px) 10px -4px rgba(0, 0, 0, 0.05); +} + +/* Panel positions: strengthen the shadow on whichever edge faces the panel */ +.monaco-workbench.panel-position-left.vs .part.editor::after { + box-shadow: + inset var(--vscode-shadow-depth-x), + inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.04); +} + +.monaco-workbench.panel-position-right.vs .part.editor::after { + box-shadow: + inset 5px 0 10px -4px rgba(0, 0, 0, 0.04), + inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.05); +} + +.monaco-workbench.panel-position-top.vs .part.editor::after { + box-shadow: + inset var(--vscode-shadow-depth-x), + inset calc(-1 * 5px) 0 10px -4px rgba(0, 0, 0, 0.04), + inset 0 var(--vscode-shadow-depth-y); +} + .monaco-workbench .part.editor > .content .editor-group-container { height: 100%; } diff --git a/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css b/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css index 6be747356305d..6f5271cc84ca6 100644 --- a/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css +++ b/src/vs/workbench/browser/parts/editor/media/modalEditorPart.css @@ -20,7 +20,7 @@ background: rgba(0, 0, 0, 0.3); .modal-editor-shadow { - box-shadow: 0 4px 32px var(--vscode-widget-shadow, rgba(0, 0, 0, 0.2)); + box-shadow: var(--vscode-shadow-xl); border-radius: 8px; overflow: hidden; } diff --git a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css index 924d9b336078e..4f9477d08657a 100644 --- a/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/multieditortabscontrol.css @@ -144,6 +144,14 @@ box-shadow: none; } +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { + box-shadow: inset var(--vscode-shadow-active-tab); +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover:not(.active) { + box-shadow: var(--vscode-shadow-sm); +} + .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab:last-child { margin-right: var(--last-tab-margin-right); /* when tabs wrap, we need a margin away from the absolute positioned editor actions */ } diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css b/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css index 9f11489cc35d8..ee7e8a6a64a0f 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css @@ -12,6 +12,7 @@ overflow: hidden; border: 1px solid var(--vscode-editorWidget-border); border-radius: var(--vscode-cornerRadius-small); + box-shadow: var(--vscode-shadow-lg); } .monaco-workbench.nostatusbar > .notifications-center { diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css b/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css index af90372abdc83..73764a1003e61 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css @@ -9,8 +9,6 @@ right: 3px; bottom: 25px; /* 22px status bar height + 3px */ display: none; - overflow: hidden; - box-shadow: 0 0 12px var(--vscode-widget-shadow); border-radius: var(--vscode-cornerRadius-small); } @@ -37,10 +35,6 @@ flex-direction: column; } -.monaco-workbench > .notifications-toasts .notification-toast-container { - overflow: hidden; /* this ensures that the notification toast does not shine through */ -} - .monaco-workbench > .notifications-toasts .notification-toast-container > .notification-toast { margin: 4px; /* enables separation and drop shadows around toasts */ transform: translate3d(0px, 100%, 0px); /* move the notification 50px to the bottom (to prevent bleed through) */ @@ -48,6 +42,10 @@ transition: transform 300ms ease-out, opacity 300ms ease-out; } +.monaco-workbench > .notifications-toasts .notifications-list-container { + box-shadow: var(--vscode-shadow-lg); +} + .monaco-workbench > .notifications-toasts .notifications-list-container, .monaco-workbench > .notifications-toasts .notification-toast-container > .notification-toast, .monaco-workbench > .notifications-toasts .notification-toast-container > .notification-toast .monaco-scrollable-element, diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index 76d5680b81f51..82f6975b6d86a 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -15,7 +15,6 @@ import { INotificationsCenterController, NotificationActionRunner } from './noti import { NotificationsList } from './notificationsList.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { $, Dimension, isAncestorOfActiveElement } from '../../../../base/browser/dom.js'; -import { widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { localize } from '../../../../nls.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; @@ -390,8 +389,6 @@ export class NotificationsCenter extends Themable implements INotificationsCente override updateStyles(): void { if (this.notificationsCenterContainer && this.notificationsCenterHeader) { - const widgetShadowColor = this.getColor(widgetShadow); - this.notificationsCenterContainer.style.boxShadow = widgetShadowColor ? `0 0 8px 2px ${widgetShadowColor}` : ''; const borderColor = this.getColor(NOTIFICATIONS_CENTER_BORDER); this.notificationsCenterContainer.style.border = borderColor ? `1px solid ${borderColor}` : ''; diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 4aa6355a41b55..bd77caab75abb 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -14,7 +14,6 @@ import { Event, Emitter } from '../../../../base/common/event.js'; import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { NOTIFICATIONS_TOAST_BORDER, NOTIFICATIONS_BACKGROUND } from '../../../common/theme.js'; import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js'; -import { widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { INotificationsToastController } from './notificationsCommands.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -553,9 +552,6 @@ export class NotificationsToasts extends Themable implements INotificationsToast const backgroundColor = this.getColor(NOTIFICATIONS_BACKGROUND); toast.style.background = backgroundColor ? backgroundColor : ''; - const widgetShadowColor = this.getColor(widgetShadow); - toast.style.boxShadow = widgetShadowColor ? `0 0 8px 2px ${widgetShadowColor}` : ''; - const borderColor = this.getColor(NOTIFICATIONS_TOAST_BORDER); toast.style.border = borderColor ? `1px solid ${borderColor}` : ''; }); diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 7faaf9e7f4b3f..1f3b102ebb13e 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -52,8 +52,7 @@ } .monaco-workbench .part.statusbar > .left-items { - flex-grow: 1; /* left items push right items to the far right end */ -} + flex-grow: 1; /* left items push right items to the far right end */} .monaco-workbench .part.statusbar > .items-container > .statusbar-item { display: inline-block; @@ -85,9 +84,13 @@ border-right: 5px solid transparent; } -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.left.first-visible-item, -.monaco-workbench .part.statusbar > .items-container > .statusbar-item.right.last-visible-item { +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.left.first-visible-item { padding-right: 0; + padding-left: 2px; +} + +.monaco-workbench .part.statusbar > .items-container > .statusbar-item.right.last-visible-item { + padding-right: 2px; padding-left: 0; } diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index cf0977721d589..a2c1090f9d151 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -7,6 +7,7 @@ .monaco-workbench .part.titlebar { display: flex; flex-direction: row; + box-shadow: var(--vscode-shadow-md); } .monaco-workbench.mac .part.titlebar { @@ -176,6 +177,7 @@ height: 22px; width: 38vw; max-width: 600px; + box-shadow: inset var(--vscode-shadow-sm); } .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center .action-item.command-center-quick-pick { diff --git a/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css b/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css index db45b5b9b4836..8ad449ee69d56 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css +++ b/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css @@ -193,7 +193,7 @@ border-radius: 4px; border: 1px solid var(--vscode-editorWidget-border); background-color: var(--vscode-editor-background); - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); max-width: 80%; text-align: center; display: none; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionApprovalModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionApprovalModel.ts new file mode 100644 index 0000000000000..5b2aa56855a12 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionApprovalModel.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; +import { Disposable, DisposableResourceMap, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorun, autorunIterableDelta, IObservable, ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { migrateLegacyTerminalToolSpecificData } from '../../common/chat.js'; +import { IChatModel } from '../../common/model/chatModel.js'; +import { IChatService, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService/chatService.js'; +import { ILanguageService } from '../../../../../editor/common/languages/language.js'; + +export interface IAgentSessionApprovalInfo { + readonly label: string; + readonly languageId: string | undefined; + confirm(): void; +} + +/** + * Tracks approval state for all live chat sessions. For each session, + * exposes an observable that emits {@link IAgentSessionApprovalInfo} + * when a tool invocation is waiting for user confirmation, or `undefined` + * when no approval is needed. + */ +export class AgentSessionApprovalModel extends Disposable { + + private readonly _approvals = new Map>(); + private readonly _modelTrackers = this._register(new DisposableResourceMap()); + + constructor( + @IChatService private readonly _chatService: IChatService, + @ILanguageService private readonly _languageService: ILanguageService, + ) { + super(); + + this._register(autorunIterableDelta( + reader => this._chatService.chatModels.read(reader), + ({ addedValues, removedValues }) => { + for (const model of addedValues) { + this._modelTrackers.set(model.sessionResource, this._trackModel(model)); + } + for (const model of removedValues) { + this._modelTrackers.deleteAndDispose(model.sessionResource); + this._approvals.get(model.sessionResource.toString())?.set(undefined, undefined); + } + } + )); + } + + getApproval(sessionResource: URI): IObservable { + return this._getOrCreateApproval(sessionResource.toString()); + } + + private _getOrCreateApproval(key: string): ISettableObservable { + let obs = this._approvals.get(key); + if (!obs) { + obs = observableValue(`sessionApproval.${key}`, undefined); + this._approvals.set(key, obs); + } + return obs; + } + + private _trackModel(model: IChatModel): IDisposable { + const settable = this._getOrCreateApproval(model.sessionResource.toString()); + + const setIfChanged = (value: IAgentSessionApprovalInfo | undefined) => { + const current = settable.get(); + if (current === value) { + return; + } + if (current !== undefined && value !== undefined && current.label === value.label && current.languageId === value.languageId) { + return; + } + settable.set(value, undefined); + }; + + return autorun(reader => { + const needsInput = model.requestNeedsInput.read(reader); + if (!needsInput) { + setIfChanged(undefined); + return; + } + + const lastResponse = model.lastRequest?.response; + if (!lastResponse?.response?.value) { + setIfChanged(undefined); + return; + } + + for (const part of lastResponse.response.value) { + if (part.kind !== 'toolInvocation') { + continue; + } + const state = part.state.read(reader); + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation || state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { + let label: string; + let languageId: string | undefined; + if (part.toolSpecificData?.kind === 'terminal') { + const terminalData = migrateLegacyTerminalToolSpecificData(part.toolSpecificData); + label = terminalData.presentationOverrides?.commandLine ?? terminalData.commandLine.forDisplay ?? terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original; + languageId = this._languageService.getLanguageIdByLanguageName(terminalData.presentationOverrides?.language ?? terminalData.language) ?? undefined; + } else if (needsInput.detail) { + label = needsInput.detail; + } else { + const msg = part.invocationMessage; + label = typeof msg === 'string' ? msg : renderAsPlaintext(msg); + } + + const confirmState = state; + setIfChanged({ + label, + languageId, + confirm: () => confirmState.confirm({ type: ToolConfirmKind.UserAction }), + }); + return; + } + } + + setIfChanged(undefined); + }); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 384baff52d4e3..36d5ff8263c3a 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -11,6 +11,7 @@ import { IOpenEvent, WorkbenchCompressibleAsyncDataTree } from '../../../../../p import { $, append, EventHelper } from '../../../../../base/browser/dom.js'; import { AgentSessionSection, IAgentSession, IAgentSessionSection, IAgentSessionsModel, IMarshalledAgentSessionContext, isAgentSession, isAgentSessionSection } from './agentSessionsModel.js'; import { AgentSessionListItem, AgentSessionRenderer, AgentSessionsAccessibilityProvider, AgentSessionsCompressionDelegate, AgentSessionsDataSource, AgentSessionsDragAndDrop, AgentSessionsIdentityProvider, AgentSessionsKeyboardNavigationLabelProvider, AgentSessionsListDelegate, AgentSessionSectionRenderer, AgentSessionsSorter, IAgentSessionsFilter, IAgentSessionsSorterOptions } from './agentSessionsViewer.js'; +import { AgentSessionApprovalModel } from './agentSessionApprovalModel.js'; import { FuzzyScore } from '../../../../../base/common/filters.js'; import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; import { IChatSessionsService } from '../../common/chatSessionsService.js'; @@ -40,7 +41,9 @@ export interface IAgentSessionsControlOptions extends IAgentSessionsSorterOption readonly overrideStyles: IStyleOverride; readonly filter: IAgentSessionsFilter; readonly source: string; - readonly disableHover?: boolean; + readonly useSimpleHover?: boolean; + readonly showIsolationIcon?: boolean; + readonly enableApprovalRow?: boolean; getHoverPosition(): HoverPosition; trackActiveEditorSession(): boolean; @@ -163,13 +166,15 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo }; const sorter = new AgentSessionsSorter(this.options); + const approvalModel = this.options.enableApprovalRow ? this._register(this.instantiationService.createInstance(AgentSessionApprovalModel)) : undefined; + const sessionRenderer = this._register(this.instantiationService.createInstance(AgentSessionRenderer, this.options, approvalModel)); const list = this.sessionsList = this._register(this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'AgentSessionsView', this.sessionsContainer, - new AgentSessionsListDelegate(), + new AgentSessionsListDelegate(approvalModel), new AgentSessionsCompressionDelegate(), [ - this._register(this.instantiationService.createInstance(AgentSessionRenderer, this.options)), + sessionRenderer, this.instantiationService.createInstance(AgentSessionSectionRenderer), ], new AgentSessionsDataSource(this.options.filter, sorter), @@ -191,6 +196,12 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo ChatContextKeys.agentSessionsViewerFocused.bindTo(list.contextKeyService); + this._register(sessionRenderer.onDidChangeItemHeight(session => { + if (list.hasNode(session)) { + list.updateElementHeight(session, undefined); + } + })); + const model = this.agentSessionsService.model; this._register(this.options.filter.onDidChange(async () => { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index eb1748803f038..c49765298f0d8 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -37,12 +37,18 @@ import { MenuId } from '../../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; -import { Event } from '../../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; import { MarkdownString, IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { AgentSessionHoverWidget } from './agentSessionHoverWidget.js'; import { AgentSessionProviders, getAgentSessionTime } from './agentSessions.js'; import { AgentSessionsGrouping } from './agentSessionsFilter.js'; +import { autorun } from '../../../../../base/common/observable.js'; +import { Button } from '../../../../../base/browser/ui/button/button.js'; +import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; +import { AgentSessionApprovalModel } from './agentSessionApprovalModel.js'; +import { BugIndicatingError } from '../../../../../base/common/errors.js'; + export type AgentSessionListItem = IAgentSession | IAgentSessionSection; @@ -70,13 +76,19 @@ interface IAgentSessionItemTemplate { readonly separator: HTMLElement; readonly description: HTMLElement; + // Approval row + readonly approvalRow: HTMLElement; + readonly approvalLabel: HTMLElement; + readonly approvalButtonContainer: HTMLElement; + readonly contextKeyService: IContextKeyService; readonly elementDisposable: DisposableStore; readonly disposables: IDisposable; } export interface IAgentSessionRendererOptions { - readonly disableHover?: boolean; + readonly useSimpleHover?: boolean; + readonly showIsolationIcon?: boolean; getHoverPosition(): HoverPosition; } @@ -84,12 +96,18 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre static readonly TEMPLATE_ID = 'agent-session'; + static readonly APPROVAL_ROW_HEIGHT = 40; + readonly templateId = AgentSessionRenderer.TEMPLATE_ID; private readonly sessionHover = this._register(new MutableDisposable()); + private readonly _onDidChangeItemHeight = this._register(new Emitter()); + readonly onDidChangeItemHeight: Event = this._onDidChangeItemHeight.event; + constructor( private readonly options: IAgentSessionRendererOptions, + private readonly _approvalModel: AgentSessionApprovalModel | undefined, @IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService, @IProductService private readonly productService: IProductService, @IHoverService private readonly hoverService: IHoverService, @@ -129,6 +147,10 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre h('span.agent-session-status-time@statusTime') ]), ]), + ]), + h('div.agent-session-approval-row@approvalRow', [ + h('span.agent-session-approval-label@approvalLabel'), + h('div.agent-session-approval-button@approvalButtonContainer'), ]) ]) ] @@ -156,6 +178,9 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre statusContainer: elements.statusContainer, statusProviderIcon: elements.statusProviderIcon, statusTime: elements.statusTime, + approvalRow: elements.approvalRow, + approvalLabel: elements.approvalLabel, + approvalButtonContainer: elements.approvalButtonContainer, contextKeyService, elementDisposable, disposables @@ -229,6 +254,11 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre // Hover this.renderHover(session, template); + + // Approval row + if (this._approvalModel) { + this.renderApprovalRow(session, template); + } } private renderBadge(session: ITreeNode, template: IAgentSessionItemTemplate): boolean { @@ -353,8 +383,17 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre }; // Provider icon (only shown for non-local sessions) + // When showIsolationIcon is enabled for background sessions, show worktree/folder icon instead const isLocal = session.element.providerType === AgentSessionProviders.Local; - template.statusProviderIcon.className = isLocal ? '' : `agent-session-status-provider-icon ${ThemeIcon.asClassName(session.element.icon)}`; + if (isLocal) { + template.statusProviderIcon.className = ''; + } else if (this.options.showIsolationIcon && session.element.providerType === AgentSessionProviders.Background) { + const hasWorktree = typeof session.element.metadata?.worktreePath === 'string'; + const isolationIcon = hasWorktree ? Codicon.worktree : Codicon.folder; + template.statusProviderIcon.className = `agent-session-status-provider-icon ${ThemeIcon.asClassName(isolationIcon)}`; + } else { + template.statusProviderIcon.className = `agent-session-status-provider-icon ${ThemeIcon.asClassName(session.element.icon)}`; + } // Time label template.statusTime.textContent = getTimeLabel(session.element); @@ -363,7 +402,9 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre } private renderHover(session: ITreeNode, template: IAgentSessionItemTemplate): void { - if (this.options.disableHover) { + if (this.options.useSimpleHover) { + const title = renderAsPlaintext(new MarkdownString(session.element.label)); + template.elementDisposable.add(this.hoverService.setupDelayedHover(template.element, { content: title, position: { hoverPosition: this.options.getHoverPosition() } }, { groupId: 'agent.sessions' })); return; } @@ -396,6 +437,55 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre }; } + private renderApprovalRow(session: ITreeNode, template: IAgentSessionItemTemplate): void { + if (this._approvalModel === undefined) { + throw new BugIndicatingError('Approval model is required to render approval row'); + } + + const approvalModel = this._approvalModel; + // Initialize from current model state to avoid unnecessary height changes on first render + const initialInfo = approvalModel.getApproval(session.element.resource).get(); + let wasVisible = !!initialInfo; + template.approvalRow.classList.toggle('visible', wasVisible); + + const buttonStore = template.elementDisposable.add(new DisposableStore()); + + template.elementDisposable.add(autorun(reader => { + buttonStore.clear(); + + const info = approvalModel.getApproval(session.element.resource).read(reader); + const visible = !!info; + + template.approvalRow.classList.toggle('visible', visible); + + if (info) { + // Render as a syntax-highlighted code block + const codeblockContent = new MarkdownString().appendCodeblock(info.languageId ?? 'json', info.label); + this.renderMarkdownOrText(codeblockContent, template.approvalLabel, buttonStore); + + // Hover with full content as a code block + buttonStore.add(this.hoverService.setupDelayedHover(template.approvalLabel, { + content: codeblockContent, + style: HoverStyle.Pointer, + position: { hoverPosition: HoverPosition.BELOW }, + })); + + template.approvalButtonContainer.textContent = ''; + const button = buttonStore.add(new Button(template.approvalButtonContainer, { + title: localize('allowActionOnce', "Allow once"), + ...defaultButtonStyles + })); + button.label = localize('allowAction', "Allow"); + buttonStore.add(button.onDidClick(() => info.confirm())); + } + + if (wasVisible !== visible) { + wasVisible = visible; + this._onDidChangeItemHeight.fire(session.element); + } + })); + } + renderCompressedElements(node: ITreeNode, FuzzyScore>, index: number, templateData: IAgentSessionItemTemplate, details?: ITreeElementRenderDetails): void { throw new Error('Should never happen since session is incompressible'); } @@ -509,12 +599,22 @@ export class AgentSessionsListDelegate implements IListVirtualDelegate .rendered-markdown, + & > .rendered-markdown > .code, + & > .rendered-markdown > .code > span { + display: block; + overflow: hidden; + } + + .monaco-tokenized-source { + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 12px; + } + } + + .agent-session-approval-button { + flex-shrink: 0; + + .monaco-button { + padding: 2px 10px; + font-size: 12px; + white-space: nowrap; + } + } + } } .agent-session-section { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css index a29b827631234..0c26b35ff4933 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css @@ -14,19 +14,19 @@ justify-content: center; gap: 4px; z-index: 10; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-md); overflow: hidden; } @keyframes pulse { 0% { - box-shadow: 0 2px 8px 0 var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-md); } 50% { - box-shadow: 0 2px 8px 4px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); } 100% { - box-shadow: 0 2px 8px 0 var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-md); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingExplanationWidget.css b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingExplanationWidget.css index 7bfca698ff817..71dbdf0a6d7eb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingExplanationWidget.css +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingExplanationWidget.css @@ -11,7 +11,7 @@ background-color: var(--vscode-editorWidget-background); border: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder)); border-radius: 8px; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); font-size: 12px; line-height: 1.4; opacity: 0; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css index 402bd4b6b0b73..e24c87525bf8b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css @@ -7,7 +7,7 @@ opacity: 0; transition: opacity 0.2s ease-in-out; display: flex; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-md); border-radius: 6px; overflow: hidden; } diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionApprovalModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionApprovalModel.test.ts new file mode 100644 index 0000000000000..321bac437d2d9 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionApprovalModel.test.ts @@ -0,0 +1,522 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { ISettableObservable, observableValue } from '../../../../../../base/common/observable.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { AgentSessionApprovalModel, IAgentSessionApprovalInfo } from '../../../browser/agentSessions/agentSessionApprovalModel.js'; +import { MockChatModel } from '../../common/model/mockChatModel.js'; +import { MockChatService } from '../../common/chatService/mockChatService.js'; +import { IChatToolInvocation, IChatTerminalToolInvocationData, ToolConfirmKind, ConfirmedReason } from '../../../common/chatService/chatService.js'; +import { IChatModel, IChatRequestModel, IChatResponseModel, IResponse, IChatProgressResponseContent } from '../../../common/model/chatModel.js'; +import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; + +function makeToolInvocationPart(options: { + state: IChatToolInvocation.State; + toolSpecificData?: IChatToolInvocation['toolSpecificData']; + invocationMessage?: string | MarkdownString; +}): IChatToolInvocation { + return { + kind: 'toolInvocation', + presentation: undefined!, + originMessage: undefined, + invocationMessage: options.invocationMessage ?? 'Running tool...', + pastTenseMessage: undefined, + source: undefined!, + toolId: 'test-tool', + toolCallId: 'call-1', + state: observableValue('toolState', options.state), + toolSpecificData: options.toolSpecificData, + toJSON: () => undefined!, + }; +} + +function makeTerminalToolData(overrides?: Partial): IChatTerminalToolInvocationData { + return { + kind: 'terminal', + commandLine: { original: 'echo hello' }, + language: 'sh', + ...overrides, + }; +} + +function makeWaitingState(confirm?: (reason: ConfirmedReason) => void): IChatToolInvocation.State { + return { + type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters: {}, + confirm: confirm ?? (() => { }), + } as IChatToolInvocation.State; +} + +function makePostApprovalState(confirm?: (reason: ConfirmedReason) => void): IChatToolInvocation.State { + return { + type: IChatToolInvocation.StateKind.WaitingForPostApproval, + parameters: {}, + confirmed: { type: ToolConfirmKind.UserAction }, + resultDetails: undefined, + confirm: confirm ?? (() => { }), + contentForModel: [], + } as IChatToolInvocation.State; +} + +function makeExecutingState(): IChatToolInvocation.State { + return { + type: IChatToolInvocation.StateKind.Executing, + parameters: {}, + confirmed: { type: ToolConfirmKind.UserAction }, + progress: observableValue('progress', { message: undefined, progress: undefined }), + } as IChatToolInvocation.State; +} + +/** Creates a minimal mock that satisfies the response chain: lastRequest.response.response.value */ +function mockModelWithResponse(model: MockChatModel, parts: IChatProgressResponseContent[]): void { + const response: Partial = { + response: { value: parts, getMarkdown: () => '', toString: () => '' } satisfies IResponse, + }; + const request: Partial = { + response: response as IChatResponseModel, + }; + (model as { lastRequest: IChatRequestModel | undefined }).lastRequest = request as IChatRequestModel; +} + +class MockLanguageService { + getLanguageIdByLanguageName(name: string): string | undefined { + switch (name) { + case 'bash': return 'sh'; + case 'python': return 'python'; + case 'powershell': return 'pwsh'; + default: return name; + } + } +} + +suite('AgentSessionApprovalModel', () => { + + const disposables = new DisposableStore(); + let chatService: MockChatService; + let chatModelsObs: ISettableObservable>; + let langservice: MockLanguageService; + + setup(() => { + chatService = new MockChatService(); + langservice = new MockLanguageService(); + chatModelsObs = chatService.chatModels as ISettableObservable>; + }); + + teardown(() => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + function createModel(): AgentSessionApprovalModel { + const model = new AgentSessionApprovalModel(chatService, langservice as ILanguageService); + disposables.add(model); + return model; + } + + function addChatModel(uri?: URI): MockChatModel { + const chatModel = disposables.add(new MockChatModel(uri ?? URI.parse(`test://session/${Math.random()}`))); + chatModelsObs.set([...Array.from(chatModelsObs.get()), chatModel], undefined); + return chatModel; + } + + function getApproval(approvalModel: AgentSessionApprovalModel, chatModel: MockChatModel): IAgentSessionApprovalInfo | undefined { + return approvalModel.getApproval(chatModel.sessionResource).get(); + } + + test('returns undefined when no models exist', () => { + const approvalModel = createModel(); + const result = approvalModel.getApproval(URI.parse('test://nonexistent')).get(); + assert.strictEqual(result, undefined); + }); + + test('returns undefined when model has no requestNeedsInput', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); + + test('returns undefined when requestNeedsInput is set but no response exists', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); + + test('returns undefined when response has no tool invocation parts', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + mockModelWithResponse(chatModel, []); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); + + test('returns undefined when tool invocation is in Executing state', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ state: makeExecutingState() }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); + + test('returns approval info for WaitingForConfirmation state with terminal data', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData(), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + const result = getApproval(approvalModel, chatModel); + assert.deepStrictEqual({ + label: result?.label, + language: result?.languageId, + }, { + label: 'echo hello', + language: 'sh', + }); + }); + + test('returns approval info for WaitingForPostApproval state', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makePostApprovalState(), + toolSpecificData: makeTerminalToolData({ commandLine: { original: 'npm install' } }), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + const result = getApproval(approvalModel, chatModel); + assert.deepStrictEqual({ + label: result?.label, + language: result?.languageId, + }, { + label: 'npm install', + language: 'sh', + }); + }); + + test('prefers presentationOverrides.commandLine and language', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ + commandLine: { original: 'python -c "print(1)"' }, + language: 'sh', + presentationOverrides: { commandLine: 'print(1)', language: 'python' }, + }), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + const result = getApproval(approvalModel, chatModel); + assert.deepStrictEqual({ + label: result?.label, + language: result?.languageId, + }, { + label: 'print(1)', + language: 'python', + }); + }); + + test('uses forDisplay from commandLine when available', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ + commandLine: { original: 'echo raw', forDisplay: 'echo display' }, + }), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel)?.label, 'echo display'); + }); + + test('uses userEdited from commandLine when forDisplay is not set', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ + commandLine: { original: 'orig', userEdited: 'user-edited' }, + }), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel)?.label, 'user-edited'); + }); + + test('uses toolEdited from commandLine as fallback', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ + commandLine: { original: 'orig', toolEdited: 'tool-edited' }, + }), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel)?.label, 'tool-edited'); + }); + + test('uses needsInput.detail when tool is not terminal', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ state: makeWaitingState() }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test', detail: 'Custom detail message' }, undefined); + + const result = getApproval(approvalModel, chatModel); + assert.deepStrictEqual({ + label: result?.label, + language: result?.languageId, + }, { + label: 'Custom detail message', + language: undefined, + }); + }); + + test('uses invocationMessage string when no terminal data and no detail', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + invocationMessage: 'Searching files...', + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + const result = getApproval(approvalModel, chatModel); + assert.deepStrictEqual({ + label: result?.label, + language: result?.languageId, + }, { + label: 'Searching files...', + language: undefined, + }); + }); + + test('uses invocationMessage MarkdownString when no terminal data and no detail', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + invocationMessage: new MarkdownString('**Running** tool'), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel)?.label, 'Running tool'); + }); + + test('confirm() delegates to tool state confirm with UserAction', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + let confirmedWith: ConfirmedReason | undefined; + const part = makeToolInvocationPart({ + state: makeWaitingState(reason => { confirmedWith = reason; }), + toolSpecificData: makeTerminalToolData(), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + getApproval(approvalModel, chatModel)?.confirm(); + assert.deepStrictEqual(confirmedWith, { type: ToolConfirmKind.UserAction }); + }); + + test('reacts to requestNeedsInput becoming undefined', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData(), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + assert.ok(getApproval(approvalModel, chatModel)); + + chatModel.requestNeedsInput.set(undefined, undefined); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); + + test('reacts to tool state changing from waiting to executing', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const stateObs = observableValue('toolState', makeWaitingState()); + const part: IChatToolInvocation = { + ...makeToolInvocationPart({ state: makeWaitingState(), toolSpecificData: makeTerminalToolData() }), + state: stateObs, + }; + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + assert.ok(getApproval(approvalModel, chatModel)); + + stateObs.set(makeExecutingState(), undefined); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); + + test('tracks multiple models independently', () => { + const approvalModel = createModel(); + const chatModel1 = addChatModel(URI.parse('test://session/1')); + const chatModel2 = addChatModel(URI.parse('test://session/2')); + + const part1 = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ commandLine: { original: 'cmd1' } }), + }); + mockModelWithResponse(chatModel1, [part1]); + chatModel1.requestNeedsInput.set({ title: 'Session 1' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel1)?.label, 'cmd1'); + assert.strictEqual(getApproval(approvalModel, chatModel2), undefined); + }); + + test('clears approval when model is removed', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData(), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + assert.ok(getApproval(approvalModel, chatModel)); + + // Remove model from chatModels + chatModelsObs.set([], undefined); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); + + test('picks the first WaitingForConfirmation part when multiple parts exist', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const executingPart = makeToolInvocationPart({ state: makeExecutingState() }); + const waitingPart = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ commandLine: { original: 'second-cmd' } }), + }); + mockModelWithResponse(chatModel, [executingPart, waitingPart]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel)?.label, 'second-cmd'); + }); + + test('handles model added after approval model is created', () => { + const approvalModel = createModel(); + + // No models yet + const uri = URI.parse('test://session/late'); + assert.strictEqual(approvalModel.getApproval(uri).get(), undefined); + + // Add model later + const chatModel = addChatModel(uri); + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ commandLine: { original: 'late-cmd' } }), + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel)?.label, 'late-cmd'); + }); + + test('handles legacy terminal tool data', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + // Legacy format has `command` instead of `commandLine` + const legacyData = { kind: 'terminal' as const, command: 'legacy-cmd', language: 'bash' }; + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: legacyData, + }); + mockModelWithResponse(chatModel, [part]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + const result = getApproval(approvalModel, chatModel); + assert.deepStrictEqual({ + label: result?.label, + language: result?.languageId, + }, { + label: 'legacy-cmd', + language: 'sh', + }); + }); + + test('observable is reused for the same session resource', () => { + const approvalModel = createModel(); + const uri = URI.parse('test://session/same'); + + const obs1 = approvalModel.getApproval(uri); + const obs2 = approvalModel.getApproval(uri); + assert.strictEqual(obs1, obs2); + }); + + test('skips non-toolInvocation parts', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + const markdownPart = { kind: 'markdownContent' as const, content: new MarkdownString('hello') }; + const waitingPart = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData({ commandLine: { original: 'the-cmd' } }), + }); + mockModelWithResponse(chatModel, [markdownPart as unknown as IChatProgressResponseContent, waitingPart]); + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + + assert.strictEqual(getApproval(approvalModel, chatModel)?.label, 'the-cmd'); + }); + + test('updating requestNeedsInput triggers re-evaluation', () => { + const approvalModel = createModel(); + const chatModel = addChatModel(); + + // Initially no requestNeedsInput + const part = makeToolInvocationPart({ + state: makeWaitingState(), + toolSpecificData: makeTerminalToolData(), + }); + mockModelWithResponse(chatModel, [part]); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + + // Set requestNeedsInput + chatModel.requestNeedsInput.set({ title: 'Test' }, undefined); + assert.ok(getApproval(approvalModel, chatModel)); + + // Clear again + chatModel.requestNeedsInput.set(undefined, undefined); + assert.strictEqual(getApproval(approvalModel, chatModel), undefined); + }); +}); diff --git a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.css b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.css index cda82e4a99866..6767728af164d 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.css +++ b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.css @@ -7,7 +7,7 @@ position: absolute; background-color: var(--vscode-editorWidget-background); color: var(--vscode-editorWidget-foreground); - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); border: 2px solid var(--vscode-focusBorder); border-radius: 6px; margin-top: -1px; diff --git a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.css b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.css index 0a8d982123f12..b002f8c148dd9 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.css +++ b/src/vs/workbench/contrib/codeEditor/browser/dictation/editorDictation.css @@ -9,7 +9,7 @@ border-radius: 8px; display: flex; align-items: center; - box-shadow: 0 4px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); z-index: 1000; min-height: var(--vscode-editor-dictation-widget-height); line-height: var(--vscode-editor-dictation-widget-height); diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css index 5f1597b9de23d..6fb4864cf8723 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css @@ -30,7 +30,7 @@ transition: top 200ms linear; background-color: var(--vscode-editorWidget-background) !important; color: var(--vscode-editorWidget-foreground); - box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); border: 1px solid var(--vscode-widget-border); border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index 2e54d39fb8ebe..a801205ecc74a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -30,7 +30,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { widgetBorder, widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; +import { widgetBorder } from '../../../../platform/theme/common/colorRegistry.js'; import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js'; import { getTitleBarStyle, TitlebarStyle } from '../../../../platform/window/common/window.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; @@ -259,9 +259,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { if (this.$el) { this.$el.style.backgroundColor = this.getColor(debugToolBarBackground) || ''; - const widgetShadowColor = this.getColor(widgetShadow); - this.$el.style.boxShadow = widgetShadowColor ? `0 0 8px 2px ${widgetShadowColor}` : ''; - const contrastBorderColor = this.getColor(widgetBorder); const borderColor = this.getColor(debugToolBarBorder); diff --git a/src/vs/workbench/contrib/debug/browser/media/debugHover.css b/src/vs/workbench/contrib/debug/browser/media/debugHover.css index e7dd01a9cfbcc..f8e51e2e4de03 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugHover.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugHover.css @@ -13,6 +13,7 @@ word-break: break-all; white-space: pre; border-radius: var(--vscode-cornerRadius-large); + box-shadow: var(--vscode-shadow-lg); } .monaco-editor .debug-hover-widget .complex-value { diff --git a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css index f8a588049f092..ca34e6f77ba14 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css @@ -13,6 +13,7 @@ left: 0; top: 0; -webkit-app-region: no-drag; + box-shadow: var(--vscode-shadow-lg); } .monaco-workbench .debug-toolbar .monaco-action-bar .action-item { diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index fd3aa7ae144bd..3031aeba46db9 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -161,6 +161,11 @@ .extensions-viewlet > .extensions .extension-list-item { position: absolute; + box-shadow: var(--vscode-shadow-sm); +} + +.extensions-viewlet > .extensions .extension-list-item:hover { + box-shadow: var(--vscode-shadow-md); } .extensions-viewlet > .extensions .extension-list-item.loading { diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index 5c7b116c2bf23..6556757c59cc4 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -8,7 +8,7 @@ color: inherit; border-radius: var(--vscode-cornerRadius-large); border: 1px solid var(--vscode-inlineChat-border); - box-shadow: 0 2px 4px 0 var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); background: var(--vscode-inlineChat-background); padding-top: 3px; position: relative; diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css index 37d4268342add..de36a936fd329 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css @@ -9,7 +9,7 @@ border-radius: 8px; display: flex; align-items: center; - box-shadow: 0 4px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); cursor: pointer; min-width: var(--vscode-inline-chat-affordance-height); min-height: var(--vscode-inline-chat-affordance-height); diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css index be58df0988b27..9b8cada0767b0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css @@ -9,7 +9,7 @@ background: var(--vscode-panel-background); border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border)); border-radius: var(--vscode-cornerRadius-large); - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); z-index: 100; } @@ -108,7 +108,7 @@ justify-content: center; gap: 4px; z-index: 10; - box-shadow: 0 2px 8px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); overflow: hidden; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.css b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.css index 65208ad34b04e..4bd8e44bf11cc 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.css +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.css @@ -16,7 +16,7 @@ visibility: hidden; background-color: var(--vscode-editorWidget-background) !important; color: var(--vscode-editorWidget-foreground); - box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); + box-shadow: var(--vscode-shadow-lg); border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; } diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 54f1f0dfc782c..e0309cff01e5f 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -299,6 +299,7 @@ display: block; position: absolute; pointer-events: none; + box-shadow: inset var(--vscode-shadow-sm); } .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-insertion-indicator-top { @@ -383,6 +384,7 @@ .notebookOverlay .monaco-list-row .cell-title-toolbar { border-radius: var(--vscode-cornerRadius-medium); + box-shadow: var(--vscode-shadow-sm); } .notebookOverlay .monaco-list-row .cell-title-toolbar, diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts index 18206f1ab62ec..b6a6170b071b3 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts @@ -20,7 +20,7 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; -import { asCssVariable, editorWidgetBackground, editorWidgetForeground, widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; +import { asCssVariable, editorWidgetBackground, editorWidgetForeground } from '../../../../platform/theme/common/colorRegistry.js'; import { ScrollType } from '../../../../editor/common/editorCommon.js'; import { SearchWidget, SearchOptions } from './preferencesWidgets.js'; import { Promises, timeout } from '../../../../base/common/async.js'; @@ -171,7 +171,6 @@ export class DefineKeybindingWidget extends Widget { this._domNode.domNode.style.backgroundColor = asCssVariable(editorWidgetBackground); this._domNode.domNode.style.color = asCssVariable(editorWidgetForeground); - this._domNode.domNode.style.boxShadow = `0 2px 8px ${asCssVariable(widgetShadow)}`; this._keybindingInputWidget = this._register(this.instantiationService.createInstance(KeybindingsSearchWidget, this._domNode.domNode, { ariaLabel: message, history: new Set([]), inputBoxStyles: defaultInputBoxStyles })); this._keybindingInputWidget.startRecordingKeys(); diff --git a/src/vs/workbench/contrib/preferences/browser/media/keybindings.css b/src/vs/workbench/contrib/preferences/browser/media/keybindings.css index 3874b5b70f795..75905e2e6d444 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/keybindings.css +++ b/src/vs/workbench/contrib/preferences/browser/media/keybindings.css @@ -7,6 +7,7 @@ padding: 10px; border-radius: var(--vscode-cornerRadius-large); position: absolute; + box-shadow: var(--vscode-shadow-lg); } .defineKeybindingWidget .message { diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 280ae562aceb0..e8ab65c8a5ab7 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -295,6 +295,7 @@ pointer-events: none; z-index: 10; position: absolute; + box-shadow: var(--vscode-shadow-sm); } .settings-editor > .settings-body .settings-toc-container .monaco-list { diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 20c78c396f13e..408b771f84c24 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -38,6 +38,7 @@ height: 100%; align-items: center; flex-flow: nowrap; + box-shadow: var(--vscode-shadow-sm); } .scm-view.hide-provider-counts .scm-provider > .count, diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index a730051c4e7b9..b54f86b3ad3ac 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -520,6 +520,12 @@ export interface ITerminalService extends ITerminalInstanceHost { * @param forceSaveState Used when the window is shutting down and we need to reveal and save hideFromUser terminals */ showBackgroundTerminal(instance: ITerminalInstance, suppressSetActive?: boolean): Promise; + /** + * Moves a visible terminal instance to the background. The terminal process + * remains alive but the instance is removed from its group/editor and tracked + * internally so it can later be shown again via {@link showBackgroundTerminal}. + */ + moveToBackground(instance: ITerminalInstance): void; revealActiveTerminal(preserveFocus?: boolean): Promise; moveToEditor(source: ITerminalInstance, group?: GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE): void; moveIntoNewEditor(source: ITerminalInstance): void; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 6107058afad2d..1524e37ad3eb4 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -1233,6 +1233,45 @@ export class TerminalService extends Disposable implements ITerminalService { } } + moveToBackground(instance: ITerminalInstance): void { + // Already backgrounded + if (this._backgroundedTerminalInstances.some(bg => bg.instance === instance)) { + return; + } + + // Remove from its current location (panel group or editor) + if (instance.target === TerminalLocation.Editor) { + this._terminalEditorService.detachInstance(instance); + } else { + const group = this._terminalGroupService.getGroupForInstance(instance); + if (!group) { + return; + } + group.removeInstance(instance); + } + + instance.detachFromElement(); + + // Track in background + this._backgroundedTerminalInstances.push({ instance, terminalLocationOptions: instance.target === TerminalLocation.Editor ? { viewColumn: ACTIVE_GROUP } : undefined }); + this._backgroundedTerminalDisposables.set(instance.instanceId, [ + instance.onDisposed(instance => { + const idx = this._backgroundedTerminalInstances.findIndex(bg => bg.instance === instance); + if (idx !== -1) { + this._backgroundedTerminalInstances.splice(idx, 1); + } + const disposables = this._backgroundedTerminalDisposables.get(instance.instanceId); + if (disposables) { + dispose(disposables); + } + this._backgroundedTerminalDisposables.delete(instance.instanceId); + this._onDidDisposeInstance.fire(instance); + }) + ]); + + this._onDidChangeInstances.fire(); + } + public async showBackgroundTerminal(instance: ITerminalInstance, suppressSetActive?: boolean): Promise { const index = this._backgroundedTerminalInstances.findIndex(bg => bg.instance === instance); if (index === -1) { diff --git a/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts new file mode 100644 index 0000000000000..90ff4a6f730d3 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/agentSessionsViewer.fixture.ts @@ -0,0 +1,599 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../base/common/uri.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { mock } from '../../../../base/test/common/mock.js'; +import { FuzzyScore } from '../../../../base/common/filters.js'; +import { ITreeNode } from '../../../../base/browser/ui/tree/tree.js'; +import { observableValue } from '../../../../base/common/observable.js'; +import { IMarkdownRendererService, MarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../platform/configuration/test/common/testConfigurationService.js'; +import { EditorMarkdownCodeBlockRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/editorMarkdownCodeBlockRenderer.js'; +import { AgentSessionRenderer, AgentSessionSectionRenderer, IAgentSessionRendererOptions } from '../../../contrib/chat/browser/agentSessions/agentSessionsViewer.js'; +import { AgentSessionStatus, IAgentSession, AgentSessionSection, IAgentSessionSection } from '../../../contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { AgentSessionProviders } from '../../../contrib/chat/browser/agentSessions/agentSessions.js'; +import { AgentSessionApprovalModel, IAgentSessionApprovalInfo } from '../../../contrib/chat/browser/agentSessions/agentSessionApprovalModel.js'; +import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; +import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from './fixtureUtils.js'; + +import '../../../contrib/chat/browser/agentSessions/media/agentsessionsviewer.css'; + +// ============================================================================ +// Mock helpers +// ============================================================================ + +function createMockSession(overrides: Partial & { label: string; status: AgentSessionStatus; providerType: string }): IAgentSession { + const now = Date.now(); + return new class extends mock() { + override readonly resource = overrides.resource ?? URI.parse(`vscode-chat-session://${overrides.providerType}/session-${Math.random().toString(36).slice(2)}`); + override readonly label = overrides.label; + override readonly status = overrides.status; + override readonly providerType = overrides.providerType; + override readonly providerLabel = overrides.providerLabel ?? overrides.providerType; + override readonly icon = overrides.icon ?? Codicon.vm; + override readonly badge = overrides.badge; + override readonly description = overrides.description; + override readonly tooltip = overrides.tooltip; + override readonly changes = overrides.changes; + override readonly timing = overrides.timing ?? { + created: now - 60 * 60 * 1000, + lastRequestStarted: undefined, + lastRequestEnded: undefined, + }; + override isArchived(): boolean { return overrides.isArchived?.() ?? false; } + override setArchived(): void { } + override isRead(): boolean { return overrides.isRead?.() ?? true; } + override setRead(): void { } + }(); +} + +function wrapAsTreeNode(element: T): ITreeNode { + return { + element, + children: [], + depth: 0, + visibleChildrenCount: 0, + visibleChildIndex: 0, + collapsible: false, + collapsed: false, + visible: true, + filterData: undefined, + }; +} + +const rendererOptions: IAgentSessionRendererOptions = { + useSimpleHover: true, + getHoverPosition: () => HoverPosition.BELOW, +}; + +// ============================================================================ +// Render helpers +// ============================================================================ + +function createMockApprovalModel(sessionResource: URI, info: IAgentSessionApprovalInfo): AgentSessionApprovalModel { + const obs = observableValue('mockApproval', info); + return new class extends mock() { + override getApproval(resource: URI) { + if (resource.toString() === sessionResource.toString()) { + return obs; + } + return observableValue('mockApproval.empty', undefined); + } + }(); +} + +function renderSessionItem(ctx: ComponentFixtureContext, session: IAgentSession, approvalModel?: AgentSessionApprovalModel): void { + const { container, disposableStore } = ctx; + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: ctx.theme, + additionalServices: (reg) => { + registerWorkbenchServices(reg); + reg.define(IMarkdownRendererService, MarkdownRendererService); + reg.defineInstance(IProductService, new class extends mock() { + override readonly urlProtocol = 'vscode'; + }()); + }, + }); + + const configService = instantiationService.get(IConfigurationService) as TestConfigurationService; + configService.setUserConfiguration('editor', { fontFamily: 'monospace' }); + const markdownRendererService = instantiationService.get(IMarkdownRendererService); + markdownRendererService.setDefaultCodeBlockRenderer(instantiationService.createInstance(EditorMarkdownCodeBlockRenderer)); + + const renderer = disposableStore.add( + instantiationService.createInstance(AgentSessionRenderer, rendererOptions, approvalModel ?? undefined) + ); + + container.style.width = '350px'; + container.style.height = 'auto'; + container.style.backgroundColor = 'var(--vscode-sideBar-background)'; + container.classList.add('agent-sessions-viewer'); + + const listRow = document.createElement('div'); + listRow.classList.add('monaco-list-row'); + listRow.style.position = 'relative'; + container.appendChild(listRow); + + const template = renderer.renderTemplate(listRow); + renderer.renderElement(wrapAsTreeNode(session), 0, template); +} + +function renderSectionItem(ctx: ComponentFixtureContext, section: IAgentSessionSection): void { + const { container, disposableStore } = ctx; + + const instantiationService = createEditorServices(disposableStore, { + colorTheme: ctx.theme, + additionalServices: (reg) => { + registerWorkbenchServices(reg); + }, + }); + + const renderer = instantiationService.createInstance(AgentSessionSectionRenderer); + + container.style.width = '350px'; + container.style.height = 'auto'; + container.style.backgroundColor = 'var(--vscode-sideBar-background)'; + container.classList.add('agent-sessions-viewer'); + + const listRow = document.createElement('div'); + listRow.classList.add('monaco-list-row'); + listRow.style.position = 'relative'; + container.appendChild(listRow); + + const template = renderer.renderTemplate(listRow); + renderer.renderElement(wrapAsTreeNode(section), 0, template); +} + +// ============================================================================ +// Fixtures +// ============================================================================ + +const now = Date.now(); + +export default defineThemedFixtureGroup({ + + // --- Status variants --- + + CompletedRead: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Refactor auth middleware', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 2 * 60 * 60 * 1000, + lastRequestStarted: now - 2 * 60 * 60 * 1000, + lastRequestEnded: now - 2 * 60 * 60 * 1000 + 45 * 1000, + }, + })), + }), + + CompletedUnread: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Add unit tests for parser', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + isRead: () => false, + timing: { + created: now - 30 * 60 * 1000, + lastRequestStarted: now - 30 * 60 * 1000, + lastRequestEnded: now - 25 * 60 * 1000, + }, + })), + }), + + InProgress: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Implement dark mode toggle', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 5 * 60 * 1000, + lastRequestStarted: now - 2 * 60 * 1000, + lastRequestEnded: undefined, + }, + })), + }), + + NeedsInput: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Fix CI pipeline configuration', + status: AgentSessionStatus.NeedsInput, + providerType: AgentSessionProviders.Local, + isRead: () => false, + timing: { + created: now - 10 * 60 * 1000, + lastRequestStarted: now - 8 * 60 * 1000, + lastRequestEnded: undefined, + }, + })), + }), + + FailedWithDuration: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Deploy staging environment', + status: AgentSessionStatus.Failed, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 60 * 60 * 1000, + lastRequestStarted: now - 60 * 60 * 1000, + lastRequestEnded: now - 60 * 60 * 1000 + 3 * 60 * 1000, + }, + })), + }), + + FailedWithoutDuration: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Migrate database schema', + status: AgentSessionStatus.Failed, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 3 * 60 * 60 * 1000, + lastRequestStarted: undefined, + lastRequestEnded: undefined, + }, + })), + }), + + // --- Content variants --- + + WithDiffChanges: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Refactor settings page', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + changes: { files: 5, insertions: 142, deletions: 87 }, + timing: { + created: now - 45 * 60 * 1000, + lastRequestStarted: now - 45 * 60 * 1000, + lastRequestEnded: now - 40 * 60 * 1000, + }, + })), + }), + + WithFileChangesList: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Update API endpoints', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Background, + icon: Codicon.worktree, + changes: [ + { modifiedUri: URI.file('/src/api/routes.ts'), insertions: 25, deletions: 10 }, + { modifiedUri: URI.file('/src/api/handlers.ts'), insertions: 50, deletions: 30 }, + { modifiedUri: URI.file('/tests/api.test.ts'), insertions: 40, deletions: 5 }, + ], + timing: { + created: now - 2 * 60 * 60 * 1000, + lastRequestStarted: now - 2 * 60 * 60 * 1000, + lastRequestEnded: now - 90 * 60 * 1000, + }, + })), + }), + + WithBadge: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Optimize build pipeline', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + badge: 'PR #1234', + timing: { + created: now - 4 * 60 * 60 * 1000, + lastRequestStarted: now - 4 * 60 * 60 * 1000, + lastRequestEnded: now - 3.5 * 60 * 60 * 1000, + }, + })), + }), + + WithMarkdownBadge: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Review security patches', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Cloud, + icon: Codicon.cloud, + badge: new MarkdownString('$(shield) Secure'), + timing: { + created: now - 6 * 60 * 60 * 1000, + lastRequestStarted: now - 6 * 60 * 60 * 1000, + lastRequestEnded: now - 5.5 * 60 * 60 * 1000, + }, + })), + }), + + WithDescription: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Upgrade dependencies', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + description: 'Updated 12 packages to latest versions', + timing: { + created: now - 24 * 60 * 60 * 1000, + lastRequestStarted: now - 24 * 60 * 60 * 1000, + lastRequestEnded: now - 23.5 * 60 * 60 * 1000, + }, + })), + }), + + WithMarkdownDescription: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Fix accessibility issues', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + description: new MarkdownString('$(check) All WCAG checks passed'), + timing: { + created: now - 48 * 60 * 60 * 1000, + lastRequestStarted: now - 48 * 60 * 60 * 1000, + lastRequestEnded: now - 47 * 60 * 60 * 1000, + }, + })), + }), + + WithBadgeAndDiff: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Implement search feature', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + badge: 'draft', + changes: { files: 8, insertions: 320, deletions: 45 }, + timing: { + created: now - 3 * 60 * 60 * 1000, + lastRequestStarted: now - 3 * 60 * 60 * 1000, + lastRequestEnded: now - 2.5 * 60 * 60 * 1000, + }, + })), + }), + + // --- State variants --- + + Archived: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Old migration script', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + isArchived: () => true, + timing: { + created: now - 7 * 24 * 60 * 60 * 1000, + lastRequestStarted: now - 7 * 24 * 60 * 60 * 1000, + lastRequestEnded: now - 7 * 24 * 60 * 60 * 1000 + 10 * 60 * 1000, + }, + })), + }), + + ArchivedUnread: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Archived unread task', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Local, + isArchived: () => true, + isRead: () => false, + timing: { + created: now - 5 * 24 * 60 * 60 * 1000, + lastRequestStarted: now - 5 * 24 * 60 * 60 * 1000, + lastRequestEnded: now - 5 * 24 * 60 * 60 * 1000 + 5 * 60 * 1000, + }, + })), + }), + + // --- Provider-type variants --- + + CloudProvider: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Generate API documentation', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Cloud, + icon: Codicon.cloud, + timing: { + created: now - 90 * 60 * 1000, + lastRequestStarted: now - 90 * 60 * 1000, + lastRequestEnded: now - 80 * 60 * 1000, + }, + })), + }), + + BackgroundProvider: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Run linter across codebase', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Background, + icon: Codicon.worktree, + timing: { + created: now - 120 * 60 * 1000, + lastRequestStarted: now - 120 * 60 * 1000, + lastRequestEnded: now - 110 * 60 * 1000, + }, + })), + }), + + ClaudeProvider: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Analyze code complexity', + status: AgentSessionStatus.Completed, + providerType: AgentSessionProviders.Claude, + icon: Codicon.claude, + timing: { + created: now - 150 * 60 * 1000, + lastRequestStarted: now - 150 * 60 * 1000, + lastRequestEnded: now - 140 * 60 * 1000, + }, + })), + }), + + CloudProviderInProgress: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Build integration tests', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Cloud, + icon: Codicon.cloud, + isRead: () => false, + timing: { + created: now - 10 * 60 * 1000, + lastRequestStarted: now - 3 * 60 * 1000, + lastRequestEnded: undefined, + }, + })), + }), + + // --- In-progress with description override --- + + InProgressWithDescription: defineComponentFixture({ + render: (ctx) => renderSessionItem(ctx, createMockSession({ + label: 'Scaffold new microservice', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Background, + icon: Codicon.worktree, + description: 'Installing dependencies...', + timing: { + created: now - 5 * 60 * 1000, + lastRequestStarted: now - 60 * 1000, + lastRequestEnded: undefined, + }, + })), + }), + + // --- Section headers --- + + SectionToday: defineComponentFixture({ + render: (ctx) => renderSectionItem(ctx, { + section: AgentSessionSection.Today, + label: 'Today', + sessions: [], + }), + }), + + SectionYesterday: defineComponentFixture({ + render: (ctx) => renderSectionItem(ctx, { + section: AgentSessionSection.Yesterday, + label: 'Yesterday', + sessions: [], + }), + }), + + SectionLastWeek: defineComponentFixture({ + render: (ctx) => renderSectionItem(ctx, { + section: AgentSessionSection.Week, + label: 'Last 7 days', + sessions: [], + }), + }), + + SectionOlder: defineComponentFixture({ + render: (ctx) => renderSectionItem(ctx, { + section: AgentSessionSection.Older, + label: 'Older', + sessions: [], + }), + }), + + SectionArchived: defineComponentFixture({ + render: (ctx) => renderSectionItem(ctx, { + section: AgentSessionSection.Archived, + label: 'Archived', + sessions: [], + }), + }), + + SectionMore: defineComponentFixture({ + render: (ctx) => renderSectionItem(ctx, { + section: AgentSessionSection.More, + label: 'More', + sessions: [], + }), + }), + + // --- Approval row variants --- + + ApprovalRowJson: defineComponentFixture({ + render: (ctx) => { + const resource = URI.parse('vscode-chat-session://local/approval-json'); + const approvalModel = createMockApprovalModel(resource, { + label: '{ "action": "deleteFile", "path": "/src/old-module.ts" }', + languageId: 'json', + confirm: () => { }, + }); + renderSessionItem(ctx, createMockSession({ + resource, + label: 'Clean up deprecated modules', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 5 * 60 * 1000, + lastRequestStarted: now - 2 * 60 * 1000, + lastRequestEnded: undefined, + }, + }), approvalModel); + }, + }), + + ApprovalRowBash: defineComponentFixture({ + render: (ctx) => { + const resource = URI.parse('vscode-chat-session://local/approval-bash'); + const approvalModel = createMockApprovalModel(resource, { + label: 'npm install --save express@latest', + languageId: 'sh', + confirm: () => { }, + }); + renderSessionItem(ctx, createMockSession({ + resource, + label: 'Update server dependencies', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 3 * 60 * 1000, + lastRequestStarted: now - 60 * 1000, + lastRequestEnded: undefined, + }, + }), approvalModel); + }, + }), + + ApprovalRowPowerShell: defineComponentFixture({ + render: (ctx) => { + const resource = URI.parse('vscode-chat-session://local/approval-powershell'); + const approvalModel = createMockApprovalModel(resource, { + label: 'Start-Job -ScriptBlock { Set-Location \'c:\\some\\path\'; npm install } | Out-Null', + languageId: 'pwsh', + confirm: () => { }, + }); + renderSessionItem(ctx, createMockSession({ + resource, + label: 'Clean up old log files', + status: AgentSessionStatus.InProgress, + providerType: AgentSessionProviders.Local, + timing: { + created: now - 4 * 60 * 1000, + lastRequestStarted: now - 2 * 60 * 1000, + lastRequestEnded: undefined, + }, + }), approvalModel); + }, + }), + + ApprovalRowLongLabel: defineComponentFixture({ + render: (ctx) => { + const resource = URI.parse('vscode-chat-session://local/approval-long'); + const approvalModel = createMockApprovalModel(resource, { + label: 'rm -rf node_modules && npm cache clean --force && npm install --legacy-peer-deps --ignore-scripts', + languageId: 'sh', + confirm: () => { }, + }); + renderSessionItem(ctx, createMockSession({ + resource, + label: 'Reset and reinstall all dependencies', + status: AgentSessionStatus.NeedsInput, + providerType: AgentSessionProviders.Cloud, + icon: Codicon.cloud, + isRead: () => false, + timing: { + created: now - 10 * 60 * 1000, + lastRequestStarted: now - 5 * 60 * 1000, + lastRequestEnded: undefined, + }, + }), approvalModel); + }, + }), +}); diff --git a/src/vs/workbench/test/browser/componentFixtures/aiStats.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/aiStats.fixture.ts index 001d6010501d6..0ab26885d3edd 100644 --- a/src/vs/workbench/test/browser/componentFixtures/aiStats.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/aiStats.fixture.ts @@ -9,12 +9,14 @@ import { ISessionData } from '../../../contrib/editTelemetry/browser/editStats/a import { Random } from '../../../../editor/test/common/core/random.js'; import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'chat/' }, { AiStatsHover: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderAiStatsHover({ ...context, data: createSampleDataWithSessions() }), }), AiStatsHoverNoData: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderAiStatsHover({ ...context, data: createEmptyData() }), }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts index 0c8ab56d71f6c..2bc08dd758784 100644 --- a/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts @@ -22,34 +22,42 @@ import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGro export default defineThemedFixtureGroup({ Buttons: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderButtons, }), ButtonBar: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderButtonBar, }), Toggles: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderToggles, }), InputBoxes: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderInputBoxes, }), CountBadges: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderCountBadges, }), ActionBar: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderActionBar, }), ProgressBars: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderProgressBars, }), HighlightedLabels: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderHighlightedLabels, }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/chatProgressContentPart.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chatProgressContentPart.fixture.ts index d22153e576682..cdb3ec7edb9df 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chatProgressContentPart.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chatProgressContentPart.fixture.ts @@ -101,8 +101,9 @@ function renderProgressPart( container.appendChild(itemContainer); } -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'chat/' }, { WithSpinner: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderProgressPart( ctx, createProgressMessage('Searching workspace for relevant files...'), @@ -112,6 +113,7 @@ export default defineThemedFixtureGroup({ }), Completed: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (ctx) => renderProgressPart( ctx, createProgressMessage('Found 12 relevant files'), @@ -121,6 +123,7 @@ export default defineThemedFixtureGroup({ }), WithCustomIcon: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (ctx) => renderProgressPart( ctx, createProgressMessage('Running tests...'), @@ -130,6 +133,7 @@ export default defineThemedFixtureGroup({ }), WithInlineCode: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderProgressPart( ctx, createProgressMessage('Reading `src/vs/workbench/contrib/chat/browser/chatWidget.ts`'), @@ -139,6 +143,7 @@ export default defineThemedFixtureGroup({ }), LongMessage: defineComponentFixture({ + labels: { kind: 'animated' }, render: (ctx) => renderProgressPart( ctx, createProgressMessage('Searching across multiple workspace folders for TypeScript files matching the pattern you described, including test files and configuration'), diff --git a/src/vs/workbench/test/browser/componentFixtures/chatQuestionCarousel.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chatQuestionCarousel.fixture.ts index 383f9dea3ab8a..db26a9199e6e3 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chatQuestionCarousel.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chatQuestionCarousel.fixture.ts @@ -124,20 +124,24 @@ const multiSelectQuestion: IChatQuestion = { // Fixtures // ============================================================================ -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'chat/' }, { SingleTextQuestion: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderCarousel(context, createCarousel([textQuestion])), }), SingleSelectQuestion: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderCarousel(context, createCarousel([singleSelectQuestion])), }), MultiSelectQuestion: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderCarousel(context, createCarousel([multiSelectQuestion])), }), MultipleQuestions: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderCarousel(context, createCarousel([ textQuestion, singleSelectQuestion, @@ -146,10 +150,12 @@ export default defineThemedFixtureGroup({ }), NoSkip: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderCarousel(context, createCarousel([singleSelectQuestion], false)), }), SubmittedSummary: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => { const carousel = createCarousel([textQuestion, singleSelectQuestion, multiSelectQuestion]); carousel.isUsed = true; @@ -163,6 +169,7 @@ export default defineThemedFixtureGroup({ }), SkippedSummary: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => { const carousel = createCarousel([textQuestion, singleSelectQuestion]); carousel.isUsed = true; diff --git a/src/vs/workbench/test/browser/componentFixtures/codeActionList.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/codeActionList.fixture.ts index 76be7d9d68248..9e54ad9819612 100644 --- a/src/vs/workbench/test/browser/componentFixtures/codeActionList.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/codeActionList.fixture.ts @@ -92,11 +92,13 @@ const simpleFixes: IActionListItem[] = [ { kind: ActionListItemKind.Action, item: 'fix-3', label: 'Add \'await\' to async call', group: { title: 'Quick Fix', icon: Codicon.lightBulb } }, ]; -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'editor/' }, { GroupedCodeActions: defineComponentFixture({ + labels: { kind: 'animated' }, render: (context) => renderCodeActionList({ ...context, items: quickFixItems }), }), SimpleQuickFixes: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderCodeActionList({ ...context, items: simpleFixes }), }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/codeEditor.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/codeEditor.fixture.ts index c752df7da146c..03c64b694aa90 100644 --- a/src/vs/workbench/test/browser/componentFixtures/codeEditor.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/codeEditor.fixture.ts @@ -68,8 +68,9 @@ function renderCodeEditor({ container, disposableStore, theme }: ComponentFixtur editor.setModel(model); } -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'editor/' }, { CodeEditor: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderCodeEditor(context), }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/findWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/findWidget.fixture.ts index 4ed036840b666..c187a3e530f9f 100644 --- a/src/vs/workbench/test/browser/componentFixtures/findWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/findWidget.fixture.ts @@ -119,11 +119,13 @@ async function renderFindWidget(options: FindFixtureOptions): Promise { await new Promise(resolve => setTimeout(resolve, 300)); } -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'editor/' }, { Find: defineComponentFixture({ + labels: { kind: 'animated' }, render: (context) => renderFindWidget({ ...context, searchString: 'count' }), }), FindAndReplace: defineComponentFixture({ + labels: { kind: 'animated' }, render: (context) => renderFindWidget({ ...context, searchString: 'count', replaceString: 'value', showReplace: true }), }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts b/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts index d482e55e70190..e5318aaeecf99 100644 --- a/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts +++ b/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts @@ -474,6 +474,24 @@ export function createTextModel( // Fixture Adapters // ============================================================================ +export interface ThemedFixtureGroupLabels { + readonly kind?: 'screenshot' | 'animated'; + readonly blocksCi?: true; +} + +function resolveLabels(labels: ThemedFixtureGroupLabels | undefined): string[] { + const result: string[] = []; + if (labels?.kind === 'screenshot') { + result.push('.screenshot'); + } else if (labels?.kind === 'animated') { + result.push('animated'); + } + if (labels?.blocksCi) { + result.push('blocks-ci'); + } + return result; +} + export interface ComponentFixtureContext { container: HTMLElement; disposableStore: DisposableStore; @@ -482,6 +500,7 @@ export interface ComponentFixtureContext { export interface ComponentFixtureOptions { render: (context: ComponentFixtureContext) => void | Promise; + labels?: ThemedFixtureGroupLabels; } type ThemedFixtures = ReturnType; @@ -509,18 +528,33 @@ export function defineComponentFixture(options: ComponentFixtureOptions): Themed }, }); - return defineFixtureVariants({ + const labels = resolveLabels(options.labels); + return defineFixtureVariants(labels.length > 0 ? { labels } : {}, { Dark: createFixture(darkTheme), Light: createFixture(lightTheme), }); } -type ThemedFixtureGroupInput = Record; +interface ThemedFixtureGroupOptions { + readonly path?: string; + readonly labels?: ThemedFixtureGroupLabels; +} + +type ThemedFixtureGroupFixtures = Record; /** * Creates a nested fixture group from themed fixtures. * E.g., { MergeEditor: { Dark: ..., Light: ... } } becomes a nested group: MergeEditor > Dark/Light */ -export function defineThemedFixtureGroup(group: ThemedFixtureGroupInput): ReturnType { - return defineFixtureGroup(group); +export function defineThemedFixtureGroup(options: ThemedFixtureGroupOptions, fixtures: ThemedFixtureGroupFixtures): ReturnType; +export function defineThemedFixtureGroup(fixtures: ThemedFixtureGroupFixtures): ReturnType; +export function defineThemedFixtureGroup(optionsOrFixtures: ThemedFixtureGroupOptions | ThemedFixtureGroupFixtures, fixtures?: ThemedFixtureGroupFixtures): ReturnType { + if (fixtures) { + const options = optionsOrFixtures as ThemedFixtureGroupOptions; + return defineFixtureGroup({ + labels: resolveLabels(options.labels), + path: options.path, + }, fixtures as ThemedFixtureGroupFixtures); + } + return defineFixtureGroup(optionsOrFixtures as ThemedFixtureGroupFixtures); } diff --git a/src/vs/workbench/test/browser/componentFixtures/inlineCompletions.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/inlineCompletions.fixture.ts index 8d4fe0ae30655..8a598804fea36 100644 --- a/src/vs/workbench/test/browser/componentFixtures/inlineCompletions.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/inlineCompletions.fixture.ts @@ -107,9 +107,10 @@ function renderInlineEdit(options: InlineEditOptions): void { // Fixtures // ============================================================================ -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'editor/' }, { // Side-by-side view: Multi-line replacement SideBySideView: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderInlineEdit({ ...context, code: `function greet(name) { @@ -123,6 +124,7 @@ export default defineThemedFixtureGroup({ // Word replacement view: Single word change WordReplacementView: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderInlineEdit({ ...context, code: `class BufferData { @@ -139,6 +141,7 @@ export default defineThemedFixtureGroup({ // Insertion view: Insert new content InsertionView: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderInlineEdit({ ...context, code: `class BufferData { diff --git a/src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts index bbd8420665cdb..e9298fcf4d14c 100644 --- a/src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/inlineCompletionsExtras.fixture.ts @@ -276,17 +276,21 @@ function createLongDistanceEditor(options: { controller?.model?.get(); } -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'editor/' }, { HintsToolbar: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderHintsToolbar(context), }), HintsToolbarHovered: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderHintsToolbar({ ...context, simulateHover: true }), }), JumpToHint: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: renderJumpToHint, }), LongDistanceHint: defineComponentFixture({ + labels: { kind: 'animated' }, render: (context) => createLongDistanceEditor({ ...context, code: LONG_DISTANCE_CODE, diff --git a/src/vs/workbench/test/browser/componentFixtures/promptFilePickers.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/promptFilePickers.fixture.ts index 444777d13cd54..6e87b84b4d0b9 100644 --- a/src/vs/workbench/test/browser/componentFixtures/promptFilePickers.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/promptFilePickers.fixture.ts @@ -50,8 +50,9 @@ class FixtureQuickInputService extends QuickInputService { } } -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'chat/' }, { PromptFiles: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: context => renderPromptFilePickerFixture({ ...context, type: PromptsType.prompt, @@ -69,6 +70,7 @@ export default defineThemedFixtureGroup({ }), InstructionFilesWithAgentInstructions: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: context => renderPromptFilePickerFixture({ ...context, type: PromptsType.instructions, diff --git a/src/vs/workbench/test/browser/componentFixtures/renameWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/renameWidget.fixture.ts index 82416f4872ccd..0d85c6e0da449 100644 --- a/src/vs/workbench/test/browser/componentFixtures/renameWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/renameWidget.fixture.ts @@ -93,8 +93,9 @@ function renderRenameWidget(options: RenameFixtureOptions): void { ); } -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'editor/' }, { RenameVariable: defineComponentFixture({ + labels: { kind: 'animated' }, render: (context) => renderRenameWidget({ ...context, cursorLine: 4, @@ -105,6 +106,7 @@ export default defineThemedFixtureGroup({ }), }), RenameClass: defineComponentFixture({ + labels: { kind: 'animated' }, render: (context) => renderRenameWidget({ ...context, cursorLine: 1, diff --git a/src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts index 623650a7a8d64..a5aada2ea26ef 100644 --- a/src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/suggestWidget.fixture.ts @@ -144,8 +144,9 @@ const mixedKindCompletions: CompletionList = { ], }; -export default defineThemedFixtureGroup({ +export default defineThemedFixtureGroup({ path: 'editor/' }, { MethodCompletions: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderSuggestWidget({ ...context, code: `const element = document.getElementById('app'); @@ -159,6 +160,7 @@ if (element) { }), MixedKinds: defineComponentFixture({ + labels: { kind: 'screenshot' }, render: (context) => renderSuggestWidget({ ...context, code: '',