diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a100345f459b1..8dbae58b45f6b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -28,6 +28,7 @@ Visual Studio Code is built with a layered architecture using TypeScript, web AP The core architecture follows these principles: - **Layered architecture** - from `base`, `platform`, `editor`, to `workbench` - **Dependency injection** - Services are injected through constructor parameters + - If non-service parameters are needed, they need to come after the service parameters - **Contribution model** - Features contribute to registries and extension points - **Cross-platform compatibility** - Abstractions separate platform-specific code diff --git a/.gitignore b/.gitignore index 1dbe527cb8ade..3d97a65e02779 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ node_modules/ .build/ .vscode/extensions/**/out/ extensions/**/dist/ +src/vs/base/browser/ui/codicons/codicon/codicon.ttf /out*/ /extensions/**/out/ build/node_modules diff --git a/build/gulpfile.editor.ts b/build/gulpfile.editor.ts index 5096f8caa1e80..338c678b7de3f 100644 --- a/build/gulpfile.editor.ts +++ b/build/gulpfile.editor.ts @@ -36,6 +36,14 @@ const BUNDLED_FILE_HEADER = [ ].join('\n'); const extractEditorSrcTask = task.define('extract-editor-src', () => { + // Ensure codicon.ttf is copied from node_modules (needed when node_modules is cached and postinstall doesn't run) + const codiconSource = path.join(root, 'node_modules', '@vscode', 'codicons', 'dist', 'codicon.ttf'); + const codiconDest = path.join(root, 'src', 'vs', 'base', 'browser', 'ui', 'codicons', 'codicon', 'codicon.ttf'); + if (fs.existsSync(codiconSource)) { + fs.mkdirSync(path.dirname(codiconDest), { recursive: true }); + fs.copyFileSync(codiconSource, codiconDest); + } + const apiusages = monacoapi.execute().usageContent; const extrausages = fs.readFileSync(path.join(root, 'build', 'monaco', 'monaco.usage.recipe')).toString(); standalone.extractEditor({ diff --git a/build/npm/postinstall.ts b/build/npm/postinstall.ts index c4bbbf5296024..e94aaaf54c065 100644 --- a/build/npm/postinstall.ts +++ b/build/npm/postinstall.ts @@ -184,3 +184,21 @@ for (const dir of dirs) { child_process.execSync('git config pull.rebase merges'); child_process.execSync('git config blame.ignoreRevsFile .git-blame-ignore-revs'); + +// Copy codicon.ttf from @vscode/codicons package +const codiconSource = path.join(root, 'node_modules', '@vscode', 'codicons', 'dist', 'codicon.ttf'); +const codiconDest = path.join(root, 'src', 'vs', 'base', 'browser', 'ui', 'codicons', 'codicon', 'codicon.ttf'); + +if (!fs.existsSync(codiconSource)) { + console.error(`ERR codicon.ttf not found at ${codiconSource}`); + process.exit(1); +} + +try { + fs.mkdirSync(path.dirname(codiconDest), { recursive: true }); + fs.copyFileSync(codiconSource, codiconDest); + log('.', `Copied codicon.ttf to ${codiconDest}`); +} catch (error) { + console.error(`ERR Failed to copy codicon.ttf from ${codiconSource} to ${codiconDest}:`, error); + process.exit(1); +} diff --git a/extensions/git/package.json b/extensions/git/package.json index 47dbceb909276..36a8bcf872463 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -3592,11 +3592,12 @@ "items": { "type": "string" }, - "default": [ - "**/node_modules/**" - ], + "default": [], + "markdownDescription": "%config.worktreeIncludeFiles%", "scope": "resource", - "markdownDescription": "%config.worktreeIncludeFiles%" + "tags": [ + "experimental" + ] }, "git.alwaysShowStagedChangesResourceGroup": { "type": "boolean", diff --git a/extensions/git/src/askpass.ts b/extensions/git/src/askpass.ts index d9c852031e384..1cb1890e24245 100644 --- a/extensions/git/src/askpass.ts +++ b/extensions/git/src/askpass.ts @@ -5,10 +5,10 @@ import { window, InputBoxOptions, Uri, Disposable, workspace, QuickPickOptions, l10n, LogOutputChannel } from 'vscode'; import { IDisposable, EmptyDisposable, toDisposable, extractFilePathFromArgs } from './util'; -import * as path from 'path'; import { IIPCHandler, IIPCServer } from './ipc/ipcServer'; import { CredentialsProvider, Credentials } from './api/git'; import { ITerminalEnvironmentProvider } from './terminal'; +import { AskpassPaths } from './askpassManager'; export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider { @@ -20,23 +20,30 @@ export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider { readonly featureDescription = 'git auth provider'; - constructor(private ipc: IIPCServer | undefined, private readonly logger: LogOutputChannel) { + constructor( + private ipc: IIPCServer | undefined, + private readonly logger: LogOutputChannel, + askpassPaths: AskpassPaths + ) { if (ipc) { this.disposable = ipc.registerHandler('askpass', this); } + const askpassScript = this.ipc ? askpassPaths.askpass : askpassPaths.askpassEmpty; + const sshAskpassScript = this.ipc ? askpassPaths.sshAskpass : askpassPaths.sshAskpassEmpty; + this.env = { // GIT_ASKPASS - GIT_ASKPASS: path.join(__dirname, this.ipc ? 'askpass.sh' : 'askpass-empty.sh'), + GIT_ASKPASS: askpassScript, // VSCODE_GIT_ASKPASS VSCODE_GIT_ASKPASS_NODE: process.execPath, VSCODE_GIT_ASKPASS_EXTRA_ARGS: '', - VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js') + VSCODE_GIT_ASKPASS_MAIN: askpassPaths.askpassMain }; this.sshEnv = { // SSH_ASKPASS - SSH_ASKPASS: path.join(__dirname, this.ipc ? 'ssh-askpass.sh' : 'ssh-askpass-empty.sh'), + SSH_ASKPASS: sshAskpassScript, SSH_ASKPASS_REQUIRE: 'force' }; } diff --git a/extensions/git/src/askpassManager.ts b/extensions/git/src/askpassManager.ts new file mode 100644 index 0000000000000..15a6b2fa0e60b --- /dev/null +++ b/extensions/git/src/askpassManager.ts @@ -0,0 +1,233 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as cp from 'child_process'; +import { env, LogOutputChannel } from 'vscode'; + +/** + * Manages content-addressed copies of askpass scripts in a user-controlled folder. + * + * This solves the problem on Windows user/system setups where environment variables + * like GIT_ASKPASS point to scripts inside the VS Code installation directory, which + * changes on each update. By copying the scripts to a content-addressed location in + * user storage, the paths remain stable across updates (as long as the script contents + * don't change). + * + * This feature is only enabled on Windows user and system setups (not archive or portable) + * because those are the only configurations where the installation path changes on each update. + * + * Security considerations: + * - Scripts are placed in user-controlled storage (not TEMP to avoid TOCTOU attacks) + * - On Windows, ACLs are set to allow only the current user to modify the files + */ + +/** + * Checks if the current VS Code installation is a Windows user or system setup. + * Returns false for archive, portable, or non-Windows installations. + */ +function isWindowsUserOrSystemSetup(): boolean { + if (process.platform !== 'win32') { + return false; + } + + try { + const productJsonPath = path.join(env.appRoot, 'product.json'); + const productJson = JSON.parse(fs.readFileSync(productJsonPath, 'utf8')); + const target = productJson.target as string | undefined; + + // Target is 'user' or 'system' for Inno Setup installations. + // Archive and portable builds don't have a target property. + return target === 'user' || target === 'system'; + } catch { + // If we can't read product.json, assume not applicable + return false; + } +} + +interface SourceAskpassPaths { + askpass: string; + askpassMain: string; + sshAskpass: string; + askpassEmpty: string; + sshAskpassEmpty: string; +} + +/** + * Computes a SHA-256 hash of the combined contents of all askpass-related files. + * This hash is used to create content-addressed directories. + */ +function computeContentHash(sourcePaths: SourceAskpassPaths): string { + const hash = crypto.createHash('sha256'); + + // Hash all source files in a deterministic order + const files = [ + sourcePaths.askpass, + sourcePaths.askpassMain, + sourcePaths.sshAskpass, + sourcePaths.askpassEmpty, + sourcePaths.sshAskpassEmpty, + ]; + + for (const file of files) { + const content = fs.readFileSync(file); + hash.update(content); + // Include filename in hash to ensure different files with same content produce different hash + hash.update(path.basename(file)); + } + + return hash.digest('hex').substring(0, 16); +} + +/** + * Sets restrictive file permissions on Windows using icacls. + * Grants full control only to the current user and removes inherited permissions. + */ +async function setWindowsPermissions(filePath: string, logger: LogOutputChannel): Promise { + const username = process.env['USERNAME']; + if (!username) { + logger.warn(`[askpassManager] Cannot set Windows permissions: USERNAME not set`); + return; + } + + return new Promise((resolve) => { + // icacls /inheritance:r /grant:r ":F" + // /inheritance:r - Remove all inherited permissions + // /grant:r - Replace (not add) permissions, giving Full control to user + const args = [filePath, '/inheritance:r', '/grant:r', `${username}:F`]; + + cp.execFile('icacls', args, (error, _stdout, stderr) => { + if (error) { + logger.warn(`[askpassManager] Failed to set permissions on ${filePath}: ${error.message}`); + if (stderr) { + logger.warn(`[askpassManager] icacls stderr: ${stderr}`); + } + } else { + logger.trace(`[askpassManager] Set permissions on ${filePath}`); + } + resolve(); + }); + }); +} + +/** + * Copies a file to the destination, creating parent directories as needed. + * Sets restrictive permissions on the copied file. + */ +async function copyFileSecure( + source: string, + dest: string, + logger: LogOutputChannel +): Promise { + const content = await fs.promises.readFile(source); + await fs.promises.writeFile(dest, content); + await setWindowsPermissions(dest, logger); +} + +export interface AskpassPaths { + readonly askpass: string; + readonly askpassMain: string; + readonly sshAskpass: string; + readonly askpassEmpty: string; + readonly sshAskpassEmpty: string; +} + +/** + * Ensures that content-addressed copies of askpass scripts exist in user storage. + * Returns the paths to the content-addressed copies. + * + * @param sourceDir The directory containing the original askpass scripts (__dirname) + * @param storageDir The user-controlled storage directory (context.storageUri.fsPath) + * @param logger Logger for diagnostic output + */ +async function ensureAskpassScripts( + sourceDir: string, + storageDir: string, + logger: LogOutputChannel +): Promise { + const sourcePaths: SourceAskpassPaths = { + askpass: path.join(sourceDir, 'askpass.sh'), + askpassMain: path.join(sourceDir, 'askpass-main.js'), + sshAskpass: path.join(sourceDir, 'ssh-askpass.sh'), + askpassEmpty: path.join(sourceDir, 'askpass-empty.sh'), + sshAskpassEmpty: path.join(sourceDir, 'ssh-askpass-empty.sh'), + }; + + // Compute content hash + const contentHash = computeContentHash(sourcePaths); + logger.trace(`[askpassManager] Content hash: ${contentHash}`); + + // Create content-addressed directory + const askpassDir = path.join(storageDir, 'askpass', contentHash); + + const destPaths: AskpassPaths = { + askpass: path.join(askpassDir, 'askpass.sh'), + askpassMain: path.join(askpassDir, 'askpass-main.js'), + sshAskpass: path.join(askpassDir, 'ssh-askpass.sh'), + askpassEmpty: path.join(askpassDir, 'askpass-empty.sh'), + sshAskpassEmpty: path.join(askpassDir, 'ssh-askpass-empty.sh'), + }; + + // Check if already exists (fast path for subsequent activations) + try { + const stat = await fs.promises.stat(destPaths.askpass); + if (stat.isFile()) { + logger.trace(`[askpassManager] Using existing content-addressed askpass at ${askpassDir}`); + return destPaths; + } + } catch { + // Directory doesn't exist, create it + } + + logger.info(`[askpassManager] Creating content-addressed askpass scripts at ${askpassDir}`); + + // Create directory and set Windows ACLs + await fs.promises.mkdir(askpassDir, { recursive: true }); + await setWindowsPermissions(askpassDir, logger); + + // Copy all files + await Promise.all([ + copyFileSecure(sourcePaths.askpass, destPaths.askpass, logger), + copyFileSecure(sourcePaths.askpassMain, destPaths.askpassMain, logger), + copyFileSecure(sourcePaths.sshAskpass, destPaths.sshAskpass, logger), + copyFileSecure(sourcePaths.askpassEmpty, destPaths.askpassEmpty, logger), + copyFileSecure(sourcePaths.sshAskpassEmpty, destPaths.sshAskpassEmpty, logger), + ]); + + logger.info(`[askpassManager] Successfully created content-addressed askpass scripts`); + + return destPaths; +} + +/** + * Returns the askpass script paths. Uses content-addressed copies + * on Windows user/system setups (to keep paths stable across updates), + * otherwise returns paths relative to the source directory. + */ +export async function getAskpassPaths( + sourceDir: string, + storagePath: string | undefined, + logger: LogOutputChannel +): Promise { + // Try content-addressed paths on Windows user/system setups + if (storagePath && isWindowsUserOrSystemSetup()) { + try { + return await ensureAskpassScripts(sourceDir, storagePath, logger); + } catch (err) { + logger.error(`[askpassManager] Failed to create content-addressed askpass scripts: ${err}`); + } + } + + // Fallback to source directory paths (for development or non-Windows setups) + return { + askpass: path.join(sourceDir, 'askpass.sh'), + askpassMain: path.join(sourceDir, 'askpass-main.js'), + sshAskpass: path.join(sourceDir, 'ssh-askpass.sh'), + askpassEmpty: path.join(sourceDir, 'askpass-empty.sh'), + sshAskpassEmpty: path.join(sourceDir, 'ssh-askpass-empty.sh'), + }; +} diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 535c0f2f30e5d..b37ae9c79c5b7 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -28,6 +28,7 @@ import { GitEditSessionIdentityProvider } from './editSessionIdentityProvider'; import { GitCommitInputBoxCodeActionsProvider, GitCommitInputBoxDiagnosticsManager } from './diagnostics'; import { GitBlameController } from './blame'; import { CloneManager } from './cloneManager'; +import { getAskpassPaths } from './askpassManager'; const deactivateTasks: { (): Promise }[] = []; @@ -71,7 +72,8 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, logger.error(`[main] Failed to create git IPC: ${err}`); } - const askpass = new Askpass(ipcServer, logger); + const askpassPaths = await getAskpassPaths(__dirname, context.globalStorageUri.fsPath, logger); + const askpass = new Askpass(ipcServer, logger, askpassPaths); disposables.push(askpass); const gitEditor = new GitEditor(ipcServer); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 04b310e502446..862223a2fcaeb 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1902,7 +1902,7 @@ export class Repository implements Disposable { private async _getWorktreeIncludePaths(): Promise> { const config = workspace.getConfiguration('git', Uri.file(this.root)); - const worktreeIncludeFiles = config.get('worktreeIncludeFiles', ['**/node_modules/**']); + const worktreeIncludeFiles = config.get('worktreeIncludeFiles', []); if (worktreeIncludeFiles.length === 0) { return new Set(); diff --git a/extensions/git/src/repositoryCache.ts b/extensions/git/src/repositoryCache.ts index 5e3f8cbe59452..bf5d74b6c683b 100644 --- a/extensions/git/src/repositoryCache.ts +++ b/extensions/git/src/repositoryCache.ts @@ -10,6 +10,7 @@ import { isDescendant } from './util'; export interface RepositoryCacheInfo { workspacePath: string; // path of the workspace folder or workspace file + lastTouchedTime?: number; // timestamp when the repository was last touched } function isRepositoryCacheInfo(obj: unknown): obj is RepositoryCacheInfo { @@ -17,7 +18,8 @@ function isRepositoryCacheInfo(obj: unknown): obj is RepositoryCacheInfo { return false; } const rec = obj as Record; - return typeof rec.workspacePath === 'string'; + return typeof rec.workspacePath === 'string' && + (rec.lastOpenedTime === undefined || typeof rec.lastOpenedTime === 'number'); } export class RepositoryCache { @@ -70,7 +72,8 @@ export class RepositoryCache { } foldersLru.set(folderPathOrWorkspaceFile, { - workspacePath: folderPathOrWorkspaceFile + workspacePath: folderPathOrWorkspaceFile, + lastTouchedTime: Date.now() }); // touch entry this.lru.set(key, foldersLru); this.save(); diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index ae0cba932f510..fd3dc657eb153 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -4,7 +4,7 @@ "type": "dark", "colors": { "foreground": "#bfbfbf", - "disabledForeground": "#444444", + "disabledForeground": "#666666", "errorForeground": "#f48771", "descriptionForeground": "#888888", "icon.foreground": "#888888", @@ -46,6 +46,13 @@ "inputValidation.warningBackground": "#191A1B", "inputValidation.warningBorder": "#2A2B2CFF", "inputValidation.warningForeground": "#bfbfbf", + "gauge.foreground": "#59a4f9", + "gauge.background": "#59a4f94D", + "gauge.border": "#2A2B2CFF", + "gauge.warningForeground": "#e5ba7d", + "gauge.warningBackground": "#e5ba7d4D", + "gauge.errorForeground": "#f48771", + "gauge.errorBackground": "#f487714D", "scrollbar.shadow": "#191B1D4D", "scrollbarSlider.background": "#83848533", "scrollbarSlider.hoverBackground": "#83848566", @@ -124,7 +131,7 @@ "editorCodeLens.foreground": "#888888", "editorBracketMatch.background": "#3994BC55", "editorBracketMatch.border": "#2A2B2CFF", - "editorWidget.background": "#202122", + "editorWidget.background": "#20212266", "editorWidget.border": "#2A2B2CFF", "editorWidget.foreground": "#bfbfbf", "editorSuggestWidget.background": "#202122", @@ -229,8 +236,16 @@ "gitDecoration.stageDeletedResourceForeground": "#f48771", "quickInputTitle.background": "#202122", "quickInput.border": "#333536", - "chat.requestBubbleBackground": "#3994BC26", - "chat.requestBubbleHoverBackground": "#3994BC46" + "chat.requestBubbleBackground": "#498FAE26", + "chat.requestBubbleHoverBackground": "#498FAE46", + "charts.foreground": "#CCCCCC", + "charts.lines": "#CCCCCC80", + "charts.blue": "#59a4f9", + "charts.red": "#f48771", + "charts.yellow": "#e5ba7d", + "charts.orange": "#d18616", + "charts.green": "#89D185", + "charts.purple": "#B180D7" }, "tokenColors": [ { @@ -331,4 +346,4 @@ "customLiteral": "#DCDCAA", "numberLiteral": "#b5cea8" } -} \ No newline at end of file +} diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index 172450999487f..89b0ead93c7a0 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -8,217 +8,224 @@ "errorForeground": "#ad0707", "descriptionForeground": "#666666", "icon.foreground": "#666666", - "focusBorder": "#4466CCFF", + "focusBorder": "#1A5CFFFF", "textBlockQuote.background": "#EDEDED", "textBlockQuote.border": "#E9EAEBFF", "textCodeBlock.background": "#EDEDED", - "textLink.foreground": "#3457C0", - "textLink.activeForeground": "#3355BA", + "textLink.foreground": "#1A5CFF", + "textLink.activeForeground": "#0D4FE8", "textPreformat.foreground": "#666666", - "textSeparator.foreground": "#ebebebFF", - "button.background": "#4466CC", + "textSeparator.foreground": "#EEEEEEFF", + "button.background": "#1A5CFF", "button.foreground": "#FFFFFF", - "button.hoverBackground": "#3E61CA", - "button.border": "#E9EAEBFF", + "button.hoverBackground": "#0D4FE8", + "button.border": "#ECEDEEFF", "button.secondaryBackground": "#EDEDED", "button.secondaryForeground": "#202020", "button.secondaryHoverBackground": "#E6E6E6", "checkbox.background": "#EDEDED", "checkbox.border": "#E9EAEBFF", "checkbox.foreground": "#202020", - "dropdown.background": "#F9F9F9", + "dropdown.background": "#FFFFFF", "dropdown.border": "#D6D7D8", "dropdown.foreground": "#202020", - "dropdown.listBackground": "#FCFCFC", - "input.background": "#F9F9F9", + "dropdown.listBackground": "#FFFFFF", + "input.background": "#FFFFFF", "input.border": "#D6D7D880", "input.foreground": "#202020", "input.placeholderForeground": "#999999", - "inputOption.activeBackground": "#4466CC33", + "inputOption.activeBackground": "#1A5CFF33", "inputOption.activeForeground": "#202020", - "inputOption.activeBorder": "#E9EAEBFF", - "inputValidation.errorBackground": "#F9F9F9", - "inputValidation.errorBorder": "#E9EAEBFF", + "inputOption.activeBorder": "#ECEDEEFF", + "inputValidation.errorBackground": "#FFFFFF", + "inputValidation.errorBorder": "#ECEDEEFF", "inputValidation.errorForeground": "#202020", - "inputValidation.infoBackground": "#F9F9F9", - "inputValidation.infoBorder": "#E9EAEBFF", + "inputValidation.infoBackground": "#FFFFFF", + "inputValidation.infoBorder": "#ECEDEEFF", "inputValidation.infoForeground": "#202020", - "inputValidation.warningBackground": "#F9F9F9", - "inputValidation.warningBorder": "#E9EAEBFF", + "inputValidation.warningBackground": "#FFFFFF", + "inputValidation.warningBorder": "#ECEDEEFF", "inputValidation.warningForeground": "#202020", + "gauge.foreground": "#1A5CFF", + "gauge.background": "#1A5CFF4D", + "gauge.border": "#ECEDEEFF", + "gauge.warningForeground": "#B69500", + "gauge.warningBackground": "#B695004D", + "gauge.errorForeground": "#ad0707", + "gauge.errorBackground": "#ad07074D", "scrollbar.shadow": "#F5F6F84D", - "scrollbarSlider.background": "#4466CC33", - "scrollbarSlider.hoverBackground": "#4466CC4D", - "scrollbarSlider.activeBackground": "#4466CC4D", - "badge.background": "#4466CC", + "scrollbarSlider.background": "#1A5CFF33", + "scrollbarSlider.hoverBackground": "#1A5CFF4D", + "scrollbarSlider.activeBackground": "#1A5CFF4D", + "badge.background": "#1A5CFF", "badge.foreground": "#FFFFFF", - "progressBar.background": "#666666", - "list.activeSelectionBackground": "#4466CC26", + "progressBar.background": "#1A5CFF", + "list.activeSelectionBackground": "#1A5CFF26", "list.activeSelectionForeground": "#202020", "list.inactiveSelectionBackground": "#EDEDED", "list.inactiveSelectionForeground": "#202020", "list.hoverBackground": "#F2F2F2", "list.hoverForeground": "#202020", - "list.dropBackground": "#4466CC1A", - "list.focusBackground": "#4466CC26", + "list.dropBackground": "#1A5CFF1A", + "list.focusBackground": "#1A5CFF26", "list.focusForeground": "#202020", - "list.focusOutline": "#4466CCFF", - "list.highlightForeground": "#202020", + "list.focusOutline": "#1A5CFFFF", + "list.highlightForeground": "#1A5CFF", "list.invalidItemForeground": "#BBBBBB", "list.errorForeground": "#ad0707", "list.warningForeground": "#667309", - "activityBar.background": "#F9F9F9", + "activityBar.background": "#FFFFFF", "activityBar.foreground": "#202020", "activityBar.inactiveForeground": "#666666", - "activityBar.border": "#E9EAEBFF", - "activityBar.activeBorder": "#E9EAEBFF", - "activityBar.activeFocusBorder": "#4466CCFF", - "activityBarBadge.background": "#4466CC", + "activityBar.border": "#ECEDEEFF", + "activityBar.activeBorder": "#ECEDEEFF", + "activityBar.activeFocusBorder": "#1A5CFFFF", + "activityBarBadge.background": "#1A5CFF", "activityBarBadge.foreground": "#FFFFFF", - "sideBar.background": "#F9F9F9", + "sideBar.background": "#FFFFFF", "sideBar.foreground": "#202020", "sideBar.border": "#E9EAEBFF", "sideBarTitle.foreground": "#202020", - "sideBarSectionHeader.background": "#F9F9F9", + "sideBarSectionHeader.background": "#FFFFFF", "sideBarSectionHeader.foreground": "#202020", - "sideBarSectionHeader.border": "#E9EAEBFF", - "titleBar.activeBackground": "#F9F9F9", + "sideBarSectionHeader.border": "#ECEDEEFF", + "titleBar.activeBackground": "#FFFFFF", "titleBar.activeForeground": "#424242", - "titleBar.inactiveBackground": "#F9F9F9", + "titleBar.inactiveBackground": "#FFFFFF", "titleBar.inactiveForeground": "#666666", "titleBar.border": "#E9EAEBFF", "menubar.selectionBackground": "#EDEDED", "menubar.selectionForeground": "#202020", - "menu.background": "#FCFCFC", + "menu.background": "#FFFFFF", "menu.foreground": "#202020", - "menu.selectionBackground": "#4466CC26", + "menu.selectionBackground": "#1A5CFF26", "menu.selectionForeground": "#202020", "menu.separatorBackground": "#F4F4F4", "menu.border": "#E9EAEBFF", "commandCenter.foreground": "#202020", "commandCenter.activeForeground": "#202020", - "commandCenter.background": "#F9F9F9", + "commandCenter.background": "#FFFFFF", "commandCenter.activeBackground": "#F2F2F2", "commandCenter.border": "#D6D7D880", - "editor.background": "#FDFDFD", + "editor.background": "#F5F6F8", "editor.foreground": "#202123", "editorLineNumber.foreground": "#656668", "editorLineNumber.activeForeground": "#202123", "editorCursor.foreground": "#202123", - "editor.selectionBackground": "#4466CC26", - "editor.inactiveSelectionBackground": "#4466CC26", - "editor.selectionHighlightBackground": "#4466CC1A", - "editor.wordHighlightBackground": "#4466CC33", - "editor.wordHighlightStrongBackground": "#4466CC33", - "editor.findMatchBackground": "#4466CC4D", - "editor.findMatchHighlightBackground": "#4466CC26", + "editor.selectionBackground": "#1A5CFF26", + "editor.inactiveSelectionBackground": "#1A5CFF26", + "editor.selectionHighlightBackground": "#1A5CFF1A", + "editor.wordHighlightBackground": "#1A5CFF33", + "editor.wordHighlightStrongBackground": "#1A5CFF33", + "editor.findMatchBackground": "#1A5CFF4D", + "editor.findMatchHighlightBackground": "#1A5CFF26", "editor.findRangeHighlightBackground": "#EDEDED", "editor.hoverHighlightBackground": "#EDEDED", "editor.lineHighlightBackground": "#EDEDED55", "editor.rangeHighlightBackground": "#EDEDED", - "editorLink.activeForeground": "#4466CC", + "editorLink.activeForeground": "#1A5CFF", "editorWhitespace.foreground": "#6666664D", "editorIndentGuide.background": "#F4F4F44D", "editorIndentGuide.activeBackground": "#F4F4F4", "editorRuler.foreground": "#F4F4F4", "editorCodeLens.foreground": "#666666", - "editorBracketMatch.background": "#4466CC55", - "editorBracketMatch.border": "#E9EAEBFF", - "editorWidget.background": "#FCFCFC", - "editorWidget.border": "#E9EAEBFF", + "editorBracketMatch.background": "#1A5CFF55", + "editorBracketMatch.border": "#ECEDEEFF", + "editorWidget.background": "#FFFFFF33", + "editorWidget.border": "#ECEDEEFF", "editorWidget.foreground": "#202020", - "editorSuggestWidget.background": "#FCFCFC", - "editorSuggestWidget.border": "#E9EAEBFF", + "editorSuggestWidget.background": "#FFFFFF", + "editorSuggestWidget.border": "#ECEDEEFF", "editorSuggestWidget.foreground": "#202020", - "editorSuggestWidget.highlightForeground": "#202020", - "editorSuggestWidget.selectedBackground": "#4466CC26", - "editorHoverWidget.background": "#FCFCFC55", - "editorHoverWidget.border": "#E9EAEBFF", - "peekView.border": "#E9EAEBFF", - "peekViewEditor.background": "#F9F9F9", - "peekViewEditor.matchHighlightBackground": "#4466CC33", - "peekViewResult.background": "#EDEDED", + "editorSuggestWidget.highlightForeground": "#1A5CFF", + "editorSuggestWidget.selectedBackground": "#1A5CFF26", + "editorHoverWidget.background": "#FFFFFF55", + "editorHoverWidget.border": "#ECEDEEFF", + "peekView.border": "#1A5CFF", + "peekViewEditor.background": "#FFFFFF", + "peekViewEditor.matchHighlightBackground": "#1A5CFF33", + "peekViewResult.background": "#FFFFFF", "peekViewResult.fileForeground": "#202020", "peekViewResult.lineForeground": "#666666", - "peekViewResult.matchHighlightBackground": "#4466CC33", - "peekViewResult.selectionBackground": "#4466CC26", + "peekViewResult.matchHighlightBackground": "#1A5CFF33", + "peekViewResult.selectionBackground": "#1A5CFF26", "peekViewResult.selectionForeground": "#202020", - "peekViewTitle.background": "#EDEDED", + "peekViewTitle.background": "#FFFFFF", "peekViewTitleDescription.foreground": "#666666", "peekViewTitleLabel.foreground": "#202020", - "editorGutter.background": "#FDFDFD", + "editorGutter.background": "#F5F6F8", "editorGutter.addedBackground": "#587c0c", "editorGutter.deletedBackground": "#ad0707", "diffEditor.insertedTextBackground": "#587c0c26", "diffEditor.removedTextBackground": "#ad070726", - "editorOverviewRuler.border": "#E9EAEBFF", - "editorOverviewRuler.findMatchForeground": "#4466CC99", - "editorOverviewRuler.modifiedForeground": "#007acc", + "editorOverviewRuler.border": "#ECEDEEFF", + "editorOverviewRuler.findMatchForeground": "#1A5CFF99", + "editorOverviewRuler.modifiedForeground": "#1A5CFF", "editorOverviewRuler.addedForeground": "#587c0c", "editorOverviewRuler.deletedForeground": "#ad0707", "editorOverviewRuler.errorForeground": "#ad0707", "editorOverviewRuler.warningForeground": "#667309", - "panel.background": "#F9F9F9", - "panel.border": "#E9EAEBFF", - "panelTitle.activeBorder": "#4466CC", + "panel.background": "#FFFFFF", + "panel.border": "#ECEDEEFF", + "panelTitle.activeBorder": "#1A5CFF", "panelTitle.activeForeground": "#202020", "panelTitle.inactiveForeground": "#666666", - "statusBar.background": "#F9F9F9", + "statusBar.background": "#FFFFFF", "statusBar.foreground": "#202020", - "statusBar.border": "#E9EAEBFF", - "statusBar.focusBorder": "#4466CCFF", - "statusBar.debuggingBackground": "#4466CC", + "statusBar.border": "#ECEDEEFF", + "statusBar.focusBorder": "#1A5CFFFF", + "statusBar.debuggingBackground": "#1A5CFF", "statusBar.debuggingForeground": "#FFFFFF", - "statusBar.noFolderBackground": "#F9F9F9", + "statusBar.noFolderBackground": "#FFFFFF", "statusBar.noFolderForeground": "#202020", "statusBarItem.activeBackground": "#E0E0E0", "statusBarItem.hoverBackground": "#F2F2F2", - "statusBarItem.focusBorder": "#4466CCFF", - "statusBarItem.prominentBackground": "#4466CC", + "statusBarItem.focusBorder": "#1A5CFFFF", + "statusBarItem.prominentBackground": "#1A5CFF", "statusBarItem.prominentForeground": "#FFFFFF", - "statusBarItem.prominentHoverBackground": "#4466CC", - "tab.activeBackground": "#FDFDFD", + "statusBarItem.prominentHoverBackground": "#1A5CFF", + "tab.activeBackground": "#F5F6F8", "tab.activeForeground": "#202020", - "tab.inactiveBackground": "#F9F9F9", + "tab.inactiveBackground": "#FFFFFF", "tab.inactiveForeground": "#666666", - "tab.border": "#E9EAEBFF", - "tab.lastPinnedBorder": "#E9EAEBFF", - "tab.activeBorder": "#FBFBFD", + "tab.border": "#ECEDEEFF", + "tab.lastPinnedBorder": "#ECEDEEFF", + "tab.activeBorder": "#F5F6F8", "tab.hoverBackground": "#F2F2F2", "tab.hoverForeground": "#202020", - "tab.unfocusedActiveBackground": "#FDFDFD", + "tab.unfocusedActiveBackground": "#F5F6F8", "tab.unfocusedActiveForeground": "#666666", - "tab.unfocusedInactiveBackground": "#F9F9F9", + "tab.unfocusedInactiveBackground": "#FFFFFF", "tab.unfocusedInactiveForeground": "#BBBBBB", - "editorGroupHeader.tabsBackground": "#F9F9F9", - "editorGroupHeader.tabsBorder": "#E9EAEBFF", + "editorGroupHeader.tabsBackground": "#FFFFFF", + "editorGroupHeader.tabsBorder": "#ECEDEEFF", "breadcrumb.foreground": "#666666", - "breadcrumb.background": "#FDFDFD", + "breadcrumb.background": "#F5F6F8", "breadcrumb.focusForeground": "#202020", "breadcrumb.activeSelectionForeground": "#202020", - "breadcrumbPicker.background": "#FCFCFC", - "notificationCenter.border": "#E9EAEBFF", + "breadcrumbPicker.background": "#FFFFFF", + "notificationCenter.border": "#ECEDEEFF", "notificationCenterHeader.foreground": "#202020", - "notificationCenterHeader.background": "#EDEDED", - "notificationToast.border": "#E9EAEBFF", + "notificationCenterHeader.background": "#FFFFFF", + "notificationToast.border": "#ECEDEEFF", "notifications.foreground": "#202020", - "notifications.background": "#FCFCFC", - "notifications.border": "#E9EAEBFF", - "notificationLink.foreground": "#4466CC", - "extensionButton.prominentBackground": "#4466CC", + "notifications.background": "#FFFFFF", + "notifications.border": "#ECEDEEFF", + "notificationLink.foreground": "#1A5CFF", + "extensionButton.prominentBackground": "#1A5CFF", "extensionButton.prominentForeground": "#FFFFFF", - "extensionButton.prominentHoverBackground": "#3E61CA", - "pickerGroup.border": "#E9EAEBFF", + "extensionButton.prominentHoverBackground": "#0D4FE8", + "pickerGroup.border": "#ECEDEEFF", "pickerGroup.foreground": "#202020", - "quickInput.background": "#FCFCFC", + "quickInput.background": "#FFFFFF", "quickInput.foreground": "#202020", - "quickInputList.focusBackground": "#4466CC26", + "quickInputList.focusBackground": "#1A5CFF26", "quickInputList.focusForeground": "#202020", "quickInputList.focusIconForeground": "#202020", "quickInputList.hoverBackground": "#E7E7E7", - "terminal.selectionBackground": "#4466CC33", + "terminal.selectionBackground": "#1A5CFF33", "terminalCursor.foreground": "#202020", - "terminalCursor.background": "#F9F9F9", + "terminalCursor.background": "#FFFFFF", "gitDecoration.addedResourceForeground": "#587c0c", "gitDecoration.modifiedResourceForeground": "#667309", "gitDecoration.deletedResourceForeground": "#ad0707", @@ -227,10 +234,18 @@ "gitDecoration.conflictingResourceForeground": "#ad0707", "gitDecoration.stageModifiedResourceForeground": "#667309", "gitDecoration.stageDeletedResourceForeground": "#ad0707", - "quickInputTitle.background": "#FCFCFC", + "quickInputTitle.background": "#FFFFFF", "quickInput.border": "#D6D7D8", - "chat.requestBubbleBackground": "#4466CC1A", - "chat.requestBubbleHoverBackground": "#4466CC26" + "chat.requestBubbleBackground": "#1A5CFF1A", + "chat.requestBubbleHoverBackground": "#1A5CFF26", + "charts.foreground": "#202020", + "charts.lines": "#20202080", + "charts.blue": "#1A5CFF", + "charts.red": "#ad0707", + "charts.yellow": "#667309", + "charts.orange": "#d18616", + "charts.green": "#388A34", + "charts.purple": "#652D90" }, "tokenColors": [ { @@ -331,4 +346,4 @@ "customLiteral": "#795E26", "numberLiteral": "#098658" } -} \ No newline at end of file +} diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index fd7ffafb11af8..fc1228f279030 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -156,7 +156,11 @@ .monaco-workbench.vs .monaco-editor .suggest-widget { background: rgba(252, 252, 253, 0.85) !important; } /* Find Widget */ -.monaco-workbench .monaco-editor .find-widget { box-shadow: 0 0 8px rgba(0, 0, 0, 0.12); border: none; border-radius: 8px; } +.monaco-workbench .monaco-editor .find-widget { + box-shadow: 0 0 8px rgba(0, 0, 0, 0.12); border: none; border-radius: 8px; + backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); + top: 4px !important; +} /* Dialog */ .monaco-workbench .monaco-dialog-box { box-shadow: 0 0 20px rgba(0, 0, 0, 0.18); border: none; border-radius: 12px; overflow: hidden; } @@ -182,7 +186,6 @@ .monaco-workbench .extensions-list .extension-list-item:hover { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10); } /* Breadcrumbs */ -.monaco-workbench .part.editor > .content .editor-group-container > .title .breadcrumbs-control { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08) !important; background: rgba(252, 252, 253, 0.65) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; } .monaco-workbench.vs-dark .part.editor > .content .editor-group-container > .title .breadcrumbs-control, /* Input Boxes */ diff --git a/package-lock.json b/package-lock.json index 532300c6956bf..cf7940c36d8a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,12 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.4", "@types/semver": "^7.5.8", + "@vscode/codicons": "^0.0.44", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", "@vscode/policy-watcher": "^1.3.2", - "@vscode/proxy-agent": "^0.36.0", + "@vscode/proxy-agent": "^0.37.0", "@vscode/ripgrep": "^1.15.13", "@vscode/spdlog": "^0.15.2", "@vscode/sqlite3": "5.1.12-vscode", @@ -2946,6 +2947,12 @@ "win32" ] }, + "node_modules/@vscode/codicons": { + "version": "0.0.44", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.44.tgz", + "integrity": "sha512-F7qPRumUK3EHjNdopfICLGRf3iNPoZQt+McTHAn4AlOWPB3W2kL4H0S7uqEqbyZ6rCxaeDjpAn3MCUnwTu/VJQ==", + "license": "CC-BY-4.0" + }, "node_modules/@vscode/deviceid": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/@vscode/deviceid/-/deviceid-0.1.4.tgz", @@ -3358,9 +3365,9 @@ } }, "node_modules/@vscode/proxy-agent": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.36.0.tgz", - "integrity": "sha512-W4mls/+zErqTYcKC41utdmoYnBWZRH1dRF9U4cBAyKU5EhcnWfVsPBvUnXXw1CffI3djmMWnu9JrF/Ynw7lkcg==", + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.37.0.tgz", + "integrity": "sha512-FDBc/3qf7fLMp4fmdRBav2dy3UZ/Vao4PN6a5IeTYvcgh9erd9HfOcVoU3ogy2uwCii6vZNvmEeF9+gr64spVQ==", "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", @@ -11731,10 +11738,11 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" }, "node_modules/lodash-es": { "version": "4.17.22", diff --git a/package.json b/package.json index da6081bc2d24d..3fc2a6c88cbf7 100644 --- a/package.json +++ b/package.json @@ -78,11 +78,12 @@ "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.4", "@types/semver": "^7.5.8", + "@vscode/codicons": "^0.0.44", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", "@vscode/policy-watcher": "^1.3.2", - "@vscode/proxy-agent": "^0.36.0", + "@vscode/proxy-agent": "^0.37.0", "@vscode/ripgrep": "^1.15.13", "@vscode/spdlog": "^0.15.2", "@vscode/sqlite3": "5.1.12-vscode", diff --git a/remote/package-lock.json b/remote/package-lock.json index c53948ab5ce8f..48d67c84737ad 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -14,7 +14,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", - "@vscode/proxy-agent": "^0.36.0", + "@vscode/proxy-agent": "^0.37.0", "@vscode/ripgrep": "^1.15.13", "@vscode/spdlog": "^0.15.2", "@vscode/tree-sitter-wasm": "^0.3.0", @@ -417,9 +417,9 @@ "license": "MIT" }, "node_modules/@vscode/proxy-agent": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.36.0.tgz", - "integrity": "sha512-W4mls/+zErqTYcKC41utdmoYnBWZRH1dRF9U4cBAyKU5EhcnWfVsPBvUnXXw1CffI3djmMWnu9JrF/Ynw7lkcg==", + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.37.0.tgz", + "integrity": "sha512-FDBc/3qf7fLMp4fmdRBav2dy3UZ/Vao4PN6a5IeTYvcgh9erd9HfOcVoU3ogy2uwCii6vZNvmEeF9+gr64spVQ==", "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", diff --git a/remote/package.json b/remote/package.json index 8d18d28dd394a..16ddeb22af32c 100644 --- a/remote/package.json +++ b/remote/package.json @@ -9,7 +9,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/native-watchdog": "^1.4.6", - "@vscode/proxy-agent": "^0.36.0", + "@vscode/proxy-agent": "^0.37.0", "@vscode/ripgrep": "^1.15.13", "@vscode/spdlog": "^0.15.2", "@vscode/tree-sitter-wasm": "^0.3.0", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 78bef5ccc5d5f..83542d2fc50c5 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", + "@vscode/codicons": "^0.0.44", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", @@ -71,6 +72,12 @@ "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, + "node_modules/@vscode/codicons": { + "version": "0.0.44", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.44.tgz", + "integrity": "sha512-F7qPRumUK3EHjNdopfICLGRf3iNPoZQt+McTHAn4AlOWPB3W2kL4H0S7uqEqbyZ6rCxaeDjpAn3MCUnwTu/VJQ==", + "license": "CC-BY-4.0" + }, "node_modules/@vscode/iconv-lite-umd": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.1.tgz", diff --git a/remote/web/package.json b/remote/web/package.json index e91442d01d77f..13438c634deb2 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,6 +5,7 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", + "@vscode/codicons": "^0.0.44", "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", diff --git a/src/vs/base/browser/ui/codicons/codicon/README.md b/src/vs/base/browser/ui/codicons/codicon/README.md new file mode 100644 index 0000000000000..8c0ffcb3b8778 --- /dev/null +++ b/src/vs/base/browser/ui/codicons/codicon/README.md @@ -0,0 +1,5 @@ +# Codicons + +## Where does the codicon.ttf come from? + +It is added via the `@vscode/codicons` npm package, then copied to this directory during the postinstall script. diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf deleted file mode 100644 index 0c9d65f81c2f3..0000000000000 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and /dev/null differ diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts index 6b5c45ccd79b0..cce59aaa131ce 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts @@ -8,15 +8,17 @@ import { IJSONSchema, TypeFromJsonSchema } from '../../../../base/common/jsonSch import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { escapeRegExpCharacters } from '../../../../base/common/strings.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; -import { EditorAction, EditorCommand, ServicesAccessor } from '../../../browser/editorExtensions.js'; +import { EditorAction, EditorAction2, EditorCommand, ServicesAccessor } from '../../../browser/editorExtensions.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; import { autoFixCommandId, codeActionCommandId, fixAllCommandId, organizeImportsCommandId, quickFixCommandId, refactorCommandId, sourceActionCommandId } from './codeAction.js'; import * as nls from '../../../../nls.js'; +import { MenuId } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionFilter, CodeActionKind, CodeActionTriggerSource } from '../common/types.js'; import { CodeActionController } from './codeActionController.js'; import { SUPPORTED_CODE_ACTIONS } from './codeActionModel.js'; +import { Codicon } from '../../../../base/common/codicons.js'; function contextKeyForSupportedActions(kind: HierarchicalKind) { return ContextKeyExpr.regex( @@ -64,22 +66,30 @@ function triggerCodeActionsForEditorSelection( } } -export class QuickFixAction extends EditorAction { +export class QuickFixAction extends EditorAction2 { constructor() { super({ id: quickFixCommandId, - label: nls.localize2('quickfix.trigger.label', "Quick Fix..."), + title: nls.localize2('quickfix.trigger.label', "Quick Fix..."), precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider), - kbOpts: { - kbExpr: EditorContextKeys.textInputFocus, + icon: Codicon.lightBulb, + f1: true, + keybinding: { + when: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.Period, weight: KeybindingWeight.EditorContrib + }, + menu: { + id: MenuId.InlineChatEditorAffordance, + group: '0_quickfix', + order: 0, + when: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider) } }); } - public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { + override runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): void { return triggerCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available"), undefined, undefined, CodeActionTriggerSource.QuickFix); } } diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts b/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts index ca5a06c9817f8..67d30044566e7 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionContributions.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { EditorContributionInstantiation, registerEditorAction, registerEditorCommand, registerEditorContribution } from '../../../browser/editorExtensions.js'; +import { registerAction2 } from '../../../../platform/actions/common/actions.js'; import { editorConfigurationBaseNode } from '../../../common/config/editorConfigurationSchema.js'; import { AutoFixAction, CodeActionCommand, FixAllAction, OrganizeImportsAction, QuickFixAction, RefactorAction, SourceAction } from './codeActionCommands.js'; import { CodeActionController } from './codeActionController.js'; @@ -14,7 +15,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; registerEditorContribution(CodeActionController.ID, CodeActionController, EditorContributionInstantiation.Eventually); registerEditorContribution(LightBulbWidget.ID, LightBulbWidget, EditorContributionInstantiation.Lazy); -registerEditorAction(QuickFixAction); +registerAction2(QuickFixAction); registerEditorAction(RefactorAction); registerEditorAction(SourceAction); registerEditorAction(OrganizeImportsAction); diff --git a/src/vs/editor/contrib/find/browser/findWidget.css b/src/vs/editor/contrib/find/browser/findWidget.css index a8f936985138e..f1dd6191b9580 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.css +++ b/src/vs/editor/contrib/find/browser/findWidget.css @@ -11,7 +11,7 @@ overflow: hidden; line-height: 19px; transition: transform 200ms linear; - padding: 0 4px; + padding: 0 4px 0 9px; box-sizing: border-box; transform: translateY(calc(-100% - 10px)); /* shadow (10px) */ box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); @@ -151,8 +151,7 @@ } .monaco-editor .find-widget .button.left { - margin-left: 0; - margin-right: 3px; + margin: 4px 0 4px 5px; } .monaco-editor .find-widget .button.wide { @@ -164,10 +163,10 @@ .monaco-editor .find-widget .button.toggle { position: absolute; top: 0; - left: 3px; + left: 0; width: 18px; - height: 100%; - border-radius: 0; + height: -webkit-fill-available; + border-radius: var(--vscode-cornerRadius-small); box-sizing: border-box; } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 4fc359bba4685..7297c8b72676d 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -279,6 +279,7 @@ export class MenuId { static readonly ChatEditorInlineGutter = new MenuId('ChatEditorInlineGutter'); static readonly ChatEditorInlineExecute = new MenuId('ChatEditorInputExecute'); static readonly ChatEditorInlineInputSide = new MenuId('ChatEditorInputSide'); + static readonly InlineChatEditorAffordance = new MenuId('InlineChatEditorAffordance'); static readonly AccessibleView = new MenuId('AccessibleView'); static readonly MultiDiffEditorContent = new MenuId('MultiDiffEditorContent'); static readonly MultiDiffEditorFileToolbar = new MenuId('MultiDiffEditorFileToolbar'); diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 772e21dcb0e37..e32d1b03b4fc3 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -648,6 +648,7 @@ export class Menubar extends Disposable { })]; case StateType.Downloading: + case StateType.Overwriting: return [new MenuItem({ label: nls.localize('miDownloadingUpdate', "Downloading Update..."), enabled: false })]; case StateType.Downloaded: diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 199f433a462df..38ad531a08cb1 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -8,9 +8,8 @@ import { upcast } from '../../../base/common/types.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; export interface IUpdate { - // Windows and Linux: 9a19815253d91900be5ec1016e0ecc7cc9a6950 (Commit Hash). Mac: 1.54.0 (Product Version) - version: string; - productVersion?: string; + version: string; // Build commit ID + productVersion?: string; // Product version like 1.2.3 timestamp?: number; url?: string; sha256hash?: string; @@ -25,13 +24,16 @@ export interface IUpdate { * ↓ ↑ * Checking for Updates → Available for Download * ↓ - * Downloading → Ready - * ↓ ↑ - * Downloaded → Updating + * ← Overwriting + * Downloading ↑ + * → Ready + * ↓ ↑ ↓ + * Downloaded → Updating Overwriting → Downloading * * Available: There is an update available for download (linux). * Ready: Code will be updated as soon as it restarts (win32, darwin). * Downloaded: There is an update ready to be installed in the background (win32). + * Overwriting: A newer update is being downloaded to replace the pending update (darwin). */ export const enum StateType { @@ -44,6 +46,7 @@ export const enum StateType { Downloaded = 'downloaded', Updating = 'updating', Ready = 'ready', + Overwriting = 'overwriting', } export const enum UpdateType { @@ -66,12 +69,13 @@ export type Disabled = { type: StateType.Disabled; reason: DisablementReason }; export type Idle = { type: StateType.Idle; updateType: UpdateType; error?: string }; export type CheckingForUpdates = { type: StateType.CheckingForUpdates; explicit: boolean }; export type AvailableForDownload = { type: StateType.AvailableForDownload; update: IUpdate }; -export type Downloading = { type: StateType.Downloading }; -export type Downloaded = { type: StateType.Downloaded; update: IUpdate }; +export type Downloading = { type: StateType.Downloading; explicit: boolean; overwrite: boolean }; +export type Downloaded = { type: StateType.Downloaded; update: IUpdate; explicit: boolean; overwrite: boolean }; export type Updating = { type: StateType.Updating; update: IUpdate }; -export type Ready = { type: StateType.Ready; update: IUpdate }; +export type Ready = { type: StateType.Ready; update: IUpdate; explicit: boolean; overwrite: boolean }; +export type Overwriting = { type: StateType.Overwriting; explicit: boolean }; -export type State = Uninitialized | Disabled | Idle | CheckingForUpdates | AvailableForDownload | Downloading | Downloaded | Updating | Ready; +export type State = Uninitialized | Disabled | Idle | CheckingForUpdates | AvailableForDownload | Downloading | Downloaded | Updating | Ready | Overwriting; export const State = { Uninitialized: upcast({ type: StateType.Uninitialized }), @@ -79,10 +83,11 @@ export const State = { Idle: (updateType: UpdateType, error?: string): Idle => ({ type: StateType.Idle, updateType, error }), CheckingForUpdates: (explicit: boolean): CheckingForUpdates => ({ type: StateType.CheckingForUpdates, explicit }), AvailableForDownload: (update: IUpdate): AvailableForDownload => ({ type: StateType.AvailableForDownload, update }), - Downloading: upcast({ type: StateType.Downloading }), - Downloaded: (update: IUpdate): Downloaded => ({ type: StateType.Downloaded, update }), + Downloading: (explicit: boolean, overwrite: boolean): Downloading => ({ type: StateType.Downloading, explicit, overwrite }), + Downloaded: (update: IUpdate, explicit: boolean, overwrite: boolean): Downloaded => ({ type: StateType.Downloaded, update, explicit, overwrite }), Updating: (update: IUpdate): Updating => ({ type: StateType.Updating, update }), - Ready: (update: IUpdate): Ready => ({ type: StateType.Ready, update }), + Ready: (update: IUpdate, explicit: boolean, overwrite: boolean): Ready => ({ type: StateType.Ready, update, explicit, overwrite }), + Overwriting: (explicit: boolean): Overwriting => ({ type: StateType.Overwriting, explicit }), }; export interface IAutoUpdater extends Event.NodeEventEmitter { diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index ed8043f262322..cf250380b3b09 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { timeout } from '../../../base/common/async.js'; -import { CancellationToken } from '../../../base/common/cancellation.js'; +import { IntervalTimer, timeout } from '../../../base/common/async.js'; +import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; @@ -14,8 +14,18 @@ import { IProductService } from '../../product/common/productService.js'; import { IRequestService } from '../../request/common/request.js'; import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js'; -export function createUpdateURL(platform: string, quality: string, productService: IProductService): string { - return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`; +export interface IUpdateURLOptions { + readonly background?: boolean; +} + +export function createUpdateURL(baseUpdateUrl: string, platform: string, quality: string, commit: string, options?: IUpdateURLOptions): string { + const url = new URL(`${baseUpdateUrl}/api/update/${platform}/${quality}/${commit}`); + + if (options?.background) { + url.searchParams.set('bg', 'true'); + } + + return url.toString(); } export type UpdateErrorClassification = { @@ -28,9 +38,12 @@ export abstract class AbstractUpdateService implements IUpdateService { declare readonly _serviceBrand: undefined; - protected url: string | undefined; + protected quality: string | undefined; private _state: State = State.Uninitialized; + protected _overwrite: boolean = false; + private _hasCheckedForOverwriteOnQuit: boolean = false; + private readonly overwriteUpdatesCheckInterval = new IntervalTimer(); private readonly _onStateChange = new Emitter(); readonly onStateChange: Event = this._onStateChange.event; @@ -43,6 +56,15 @@ export abstract class AbstractUpdateService implements IUpdateService { this.logService.info('update#setState', state.type); this._state = state; this._onStateChange.fire(state); + + // Schedule 5-minute checks when in Ready state and overwrite is supported + if (this.supportsUpdateOverwrite) { + if (state.type === StateType.Ready) { + this.overwriteUpdatesCheckInterval.cancelAndSet(() => this.checkForOverwriteUpdates(), 5 * 60 * 1000); + } else { + this.overwriteUpdatesCheckInterval.cancel(); + } + } } constructor( @@ -51,7 +73,8 @@ export abstract class AbstractUpdateService implements IUpdateService { @IEnvironmentMainService protected environmentMainService: IEnvironmentMainService, @IRequestService protected requestService: IRequestService, @ILogService protected logService: ILogService, - @IProductService protected readonly productService: IProductService + @IProductService protected readonly productService: IProductService, + protected readonly supportsUpdateOverwrite: boolean, ) { lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen) .finally(() => this.initialize()); @@ -89,19 +112,13 @@ export abstract class AbstractUpdateService implements IUpdateService { return; } - this.url = this.buildUpdateFeedUrl(quality); - if (!this.url) { + if (!this.buildUpdateFeedUrl(quality, this.productService.commit!)) { this.setState(State.Disabled(DisablementReason.InvalidConfiguration)); this.logService.info('update#ctor - updates are disabled as the update URL is badly formed'); return; } - // hidden setting - if (this.configurationService.getValue('_update.prss')) { - const url = new URL(this.url); - url.searchParams.set('prss', 'true'); - this.url = url.toString(); - } + this.quality = quality; this.setState(State.Idle(this.getUpdateType())); @@ -174,11 +191,21 @@ export abstract class AbstractUpdateService implements IUpdateService { // noop } - quitAndInstall(): Promise { + async quitAndInstall(): Promise { this.logService.trace('update#quitAndInstall, state = ', this.state.type); if (this.state.type !== StateType.Ready) { - return Promise.resolve(undefined); + return undefined; + } + + if (this.supportsUpdateOverwrite && !this._hasCheckedForOverwriteOnQuit) { + this._hasCheckedForOverwriteOnQuit = true; + const didOverwrite = await this.checkForOverwriteUpdates(true); + + if (didOverwrite) { + this.logService.info('update#quitAndInstall(): overwrite update detected, postponing quitAndInstall'); + return; + } } this.logService.trace('update#quitAndInstall(): before lifecycle quit()'); @@ -196,19 +223,57 @@ export abstract class AbstractUpdateService implements IUpdateService { return Promise.resolve(undefined); } - async isLatestVersion(): Promise { - if (!this.url) { + private async checkForOverwriteUpdates(explicit: boolean = false): Promise { + if (this._state.type !== StateType.Ready) { + return false; + } + + const pendingUpdateCommit = this._state.update.version; + + let isLatest: boolean | undefined; + + try { + const cts = new CancellationTokenSource(); + const timeoutPromise = timeout(2000).then(() => { cts.cancel(); return undefined; }); + isLatest = await Promise.race([this.isLatestVersion(pendingUpdateCommit, cts.token), timeoutPromise]); + cts.dispose(); + } catch (error) { + this.logService.warn('update#checkForOverwriteUpdates(): failed to check for updates, proceeding with restart'); + this.logService.warn(error); + return false; + } + + if (isLatest === false && this._state.type === StateType.Ready) { + this.logService.info('update#readyStateCheck: newer update available, restarting update machinery'); + await this.cancelPendingUpdate(); + this._overwrite = true; + this.setState(State.Overwriting(explicit)); + this.doCheckForUpdates(explicit, pendingUpdateCommit); + return true; + } + + return false; + } + + async isLatestVersion(commit?: string, token: CancellationToken = CancellationToken.None): Promise { + if (!this.quality) { return undefined; } const mode = this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode'); if (mode === 'none') { - return false; + return undefined; + } + + const url = this.buildUpdateFeedUrl(this.quality, commit ?? this.productService.commit!); + + if (!url) { + return undefined; } try { - const context = await this.requestService.request({ url: this.url }, CancellationToken.None); + const context = await this.requestService.request({ url }, token); // The update server replies with 204 (No Content) when no // update is available - that's all we want to know. return context.res.statusCode === 204; @@ -236,6 +301,10 @@ export abstract class AbstractUpdateService implements IUpdateService { // noop } - protected abstract buildUpdateFeedUrl(quality: string): string | undefined; - protected abstract doCheckForUpdates(explicit: boolean): void; + protected async cancelPendingUpdate(): Promise { + // noop + } + + protected abstract buildUpdateFeedUrl(quality: string, commit: string, options?: IUpdateURLOptions): string | undefined; + protected abstract doCheckForUpdates(explicit: boolean, pendingCommit?: string): void; } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index b78ebc526fca7..537b46b1a8627 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -16,7 +16,7 @@ import { IProductService } from '../../product/common/productService.js'; import { IRequestService } from '../../request/common/request.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { IUpdate, State, StateType, UpdateType } from '../common/update.js'; -import { AbstractUpdateService, createUpdateURL, UpdateErrorClassification } from './abstractUpdateService.js'; +import { AbstractUpdateService, createUpdateURL, IUpdateURLOptions, UpdateErrorClassification } from './abstractUpdateService.js'; export class DarwinUpdateService extends AbstractUpdateService implements IRelaunchHandler { @@ -25,7 +25,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau @memoize private get onRawError(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'error', (_, message) => message); } @memoize private get onRawUpdateNotAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-not-available'); } @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available'); } - @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, releaseNotes, version, timestamp) => ({ version, productVersion: version, timestamp })); } + @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, version: string, productVersion: string, timestamp: number) => ({ version, productVersion, timestamp })); } constructor( @ILifecycleMainService lifecycleMainService: ILifecycleMainService, @@ -36,7 +36,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau @ILogService logService: ILogService, @IProductService productService: IProductService ) { - super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); + super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService, true); lifecycleMainService.setRelaunchHandler(this); } @@ -73,14 +73,9 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau this.setState(State.Idle(UpdateType.Archive, message)); } - protected buildUpdateFeedUrl(quality: string): string | undefined { - let assetID: string; - if (!this.productService.darwinUniversalAssetId) { - assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64'; - } else { - assetID = this.productService.darwinUniversalAssetId; - } - const url = createUpdateURL(assetID, quality, this.productService); + protected buildUpdateFeedUrl(quality: string, commit: string, options?: IUpdateURLOptions): string | undefined { + const assetID = this.productService.darwinUniversalAssetId ?? (process.arch === 'x64' ? 'darwin' : 'darwin-arm64'); + const url = createUpdateURL(this.productService.updateUrl!, assetID, quality, commit, options); try { electron.autoUpdater.setFeedURL({ url }); } catch (e) { @@ -91,24 +86,38 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau return url; } - protected doCheckForUpdates(explicit: boolean): void { - if (!this.url) { + override async checkForUpdates(explicit: boolean): Promise { + this.logService.trace('update#checkForUpdates, state = ', this.state.type); + + if (this.state.type !== StateType.Idle) { + return; + } + + this.doCheckForUpdates(explicit); + } + + protected doCheckForUpdates(explicit: boolean, pendingCommit?: string): void { + if (!this.quality) { return; } this.setState(State.CheckingForUpdates(explicit)); - const url = explicit ? this.url : `${this.url}?bg=true`; - electron.autoUpdater.setFeedURL({ url }); + const url = this.buildUpdateFeedUrl(this.quality, pendingCommit ?? this.productService.commit!, { background: !explicit }); + + if (!url) { + return; + } + electron.autoUpdater.checkForUpdates(); } private onUpdateAvailable(): void { - if (this.state.type !== StateType.CheckingForUpdates) { + if (this.state.type !== StateType.CheckingForUpdates && this.state.type !== StateType.Overwriting) { return; } - this.setState(State.Downloading); + this.setState(State.Downloading(this.state.explicit, this._overwrite)); } private onUpdateDownloaded(update: IUpdate): void { @@ -116,16 +125,10 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau return; } - this.setState(State.Downloaded(update)); - - type UpdateDownloadedClassification = { - owner: 'joaomoreno'; - newVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version number of the new VS Code that has been downloaded.' }; - comment: 'This is used to know how often VS Code has successfully downloaded the update.'; - }; - this.telemetryService.publicLog2<{ newVersion: String }, UpdateDownloadedClassification>('update:downloaded', { newVersion: update.version }); + this.setState(State.Downloaded(update, this.state.explicit, this._overwrite)); + this.logService.info(`Update downloaded: ${JSON.stringify(update)}`); - this.setState(State.Ready(update)); + this.setState(State.Ready(update, this.state.explicit, this._overwrite)); } private onUpdateNotAvailable(): void { diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 8550ace2f4307..6845ba2569329 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -12,7 +12,7 @@ import { INativeHostMainService } from '../../native/electron-main/nativeHostMai import { IProductService } from '../../product/common/productService.js'; import { asJson, IRequestService } from '../../request/common/request.js'; import { AvailableForDownload, IUpdate, State, UpdateType } from '../common/update.js'; -import { AbstractUpdateService, createUpdateURL } from './abstractUpdateService.js'; +import { AbstractUpdateService, createUpdateURL, IUpdateURLOptions } from './abstractUpdateService.js'; export class LinuxUpdateService extends AbstractUpdateService { @@ -25,19 +25,19 @@ export class LinuxUpdateService extends AbstractUpdateService { @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, @IProductService productService: IProductService ) { - super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); + super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService, false); } - protected buildUpdateFeedUrl(quality: string): string { - return createUpdateURL(`linux-${process.arch}`, quality, this.productService); + protected buildUpdateFeedUrl(quality: string, commit: string, options?: IUpdateURLOptions): string { + return createUpdateURL(this.productService.updateUrl!, `linux-${process.arch}`, quality, commit, options); } - protected doCheckForUpdates(explicit: boolean): void { - if (!this.url) { + protected doCheckForUpdates(explicit: boolean, _pendingCommit?: string): void { + if (!this.quality) { return; } - const url = explicit ? this.url : `${this.url}?bg=true`; + const url = this.buildUpdateFeedUrl(this.quality, this.productService.commit!, { background: !explicit }); this.setState(State.CheckingForUpdates(explicit)); this.requestService.request({ url }, CancellationToken.None) diff --git a/src/vs/platform/update/electron-main/updateService.snap.ts b/src/vs/platform/update/electron-main/updateService.snap.ts index 4ca2a34a7a92c..2a394e2bde405 100644 --- a/src/vs/platform/update/electron-main/updateService.snap.ts +++ b/src/vs/platform/update/electron-main/updateService.snap.ts @@ -162,7 +162,7 @@ export class SnapUpdateService extends AbstractUpdateService { this.setState(State.CheckingForUpdates(false)); this.isUpdateAvailable().then(result => { if (result) { - this.setState(State.Ready({ version: 'something' })); + this.setState(State.Ready({ version: 'something' }, false, false)); } else { this.setState(State.Idle(UpdateType.Snap)); } diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 5bf91910dd730..c55bf9c08ac2b 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -26,7 +26,7 @@ import { IProductService } from '../../product/common/productService.js'; import { asJson, IRequestService } from '../../request/common/request.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { AvailableForDownload, DisablementReason, IUpdate, State, StateType, UpdateType } from '../common/update.js'; -import { AbstractUpdateService, createUpdateURL, UpdateErrorClassification } from './abstractUpdateService.js'; +import { AbstractUpdateService, createUpdateURL, IUpdateURLOptions, UpdateErrorClassification } from './abstractUpdateService.js'; async function pollUntil(fn: () => boolean, millis = 1000): Promise { while (!fn()) { @@ -71,7 +71,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, @IProductService productService: IProductService ) { - super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); + super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService, false); lifecycleMainService.setRelaunchHandler(this); } @@ -152,7 +152,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun } } - protected buildUpdateFeedUrl(quality: string): string | undefined { + protected buildUpdateFeedUrl(quality: string, commit: string, options?: IUpdateURLOptions): string | undefined { let platform = `win32-${process.arch}`; if (getUpdateType() === UpdateType.Archive) { @@ -161,15 +161,15 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun platform += '-user'; } - return createUpdateURL(platform, quality, this.productService); + return createUpdateURL(this.productService.updateUrl!, platform, quality, commit, options); } protected doCheckForUpdates(explicit: boolean): void { - if (!this.url) { + if (!this.quality) { return; } - const url = explicit ? this.url : `${this.url}?bg=true`; + const url = this.buildUpdateFeedUrl(this.quality, this.productService.commit!, { background: !explicit }); this.setState(State.CheckingForUpdates(explicit)); this.requestService.request({ url }, CancellationToken.None) @@ -187,7 +187,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun return Promise.resolve(null); } - this.setState(State.Downloading); + this.setState(State.Downloading(explicit, this._overwrite)); return this.cleanup(update.version).then(() => { return this.getUpdatePackagePath(update.version).then(updatePackagePath => { @@ -206,7 +206,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun }); }).then(packagePath => { this.availableUpdate = { packagePath }; - this.setState(State.Downloaded(update)); + this.setState(State.Downloaded(update, explicit, this._overwrite)); const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); if (fastUpdatesEnabled) { @@ -214,7 +214,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun this.doApplyUpdate(); } } else { - this.setState(State.Ready(update)); + this.setState(State.Ready(update, explicit, this._overwrite)); } }); }); @@ -268,6 +268,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun } const update = this.state.update; + const explicit = this.state.explicit; this.setState(State.Updating(update)); const cachePath = await this.cachePath; @@ -292,7 +293,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun // poll for mutex-ready pollUntil(() => mutex.isActive(readyMutexName)) - .then(() => this.setState(State.Ready(update))); + .then(() => this.setState(State.Ready(update, explicit, this._overwrite))); } protected override doQuitAndInstall(): void { @@ -324,16 +325,16 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); const update: IUpdate = { version: 'unknown', productVersion: 'unknown' }; - this.setState(State.Downloading); + this.setState(State.Downloading(true, false)); this.availableUpdate = { packagePath }; - this.setState(State.Downloaded(update)); + this.setState(State.Downloaded(update, true, false)); if (fastUpdatesEnabled) { if (this.productService.target === 'user') { this.doApplyUpdate(); } } else { - this.setState(State.Ready(update)); + this.setState(State.Ready(update, true, false)); } } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index b119a46852eb6..5a58eb5125102 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1890,6 +1890,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I InteractiveSessionVoteDirection: extHostTypes.InteractiveSessionVoteDirection, ChatCopyKind: extHostTypes.ChatCopyKind, ChatSessionChangedFile: extHostTypes.ChatSessionChangedFile, + ChatSessionChangedFile2: extHostTypes.ChatSessionChangedFile2, ChatEditingSessionActionOutcome: extHostTypes.ChatEditingSessionActionOutcome, InteractiveEditorResponseFeedbackKind: extHostTypes.InteractiveEditorResponseFeedbackKind, DebugStackFrame: extHostTypes.DebugStackFrame, diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 86ea71b91c82c..81e8d303d59b3 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3437,6 +3437,10 @@ export class ChatSessionChangedFile { constructor(public readonly modifiedUri: vscode.Uri, public readonly insertions: number, public readonly deletions: number, public readonly originalUri?: vscode.Uri) { } } +export class ChatSessionChangedFile2 { + constructor(public readonly uri: vscode.Uri, public readonly originalUri: vscode.Uri | undefined, public readonly modifiedUri: vscode.Uri | undefined, public readonly insertions: number, public readonly deletions: number) { } +} + export enum ChatResponseReferencePartStatusKind { Complete = 1, Partial = 2, diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 07137d30ebb4b..92fe6a1737718 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -478,6 +478,7 @@ export class CustomMenubarControl extends MenubarControl { }); case StateType.Downloading: + case StateType.Overwriting: return toAction({ id: 'update.downloading', label: localize('DownloadingUpdate', "Downloading Update..."), enabled: false, run: () => { } }); case StateType.Downloaded: diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index 2f3f9b52751cc..2c4678c419e63 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -107,6 +107,7 @@ class ViewWelcomeController { private _enabled: boolean = false; private element: HTMLElement | undefined; private scrollableElement: DomScrollableElement | undefined; + private _wide: boolean = false; private readonly disposables = new DisposableStore(); private readonly enabledDisposables = this.disposables.add(new DisposableStore()); @@ -131,7 +132,8 @@ class ViewWelcomeController { this.element!.style.height = `${height}px`; this.element!.style.width = `${width}px`; - this.element!.classList.toggle('wide', width > 640); + this._wide = width > 640; + this.element!.classList.toggle('wide', this._wide); this.scrollableElement!.scanDomNode(); } @@ -160,6 +162,9 @@ class ViewWelcomeController { this.container.classList.add('welcome'); const viewWelcomeContainer = append(this.container, $('.welcome-view')); this.element = $('.welcome-view-content', { tabIndex: 0 }); + if (this._wide) { + this.element.classList.add('wide'); + } this.scrollableElement = new DomScrollableElement(this.element, { alwaysConsumeMouseWheel: true, horizontal: ScrollbarVisibility.Hidden, vertical: ScrollbarVisibility.Visible, }); append(viewWelcomeContainer, this.scrollableElement.getDomNode()); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 6f60564133993..548b8ee12cab0 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -21,7 +21,7 @@ import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; import { Extensions, IOutputChannelRegistry, IOutputService } from '../../../../services/output/common/output.js'; -import { ChatSessionStatus as AgentSessionStatus, IChatSessionFileChange, IChatSessionItem, IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; +import { ChatSessionStatus as AgentSessionStatus, IChatSessionFileChange, IChatSessionFileChange2, IChatSessionItem, IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName } from './agentSessions.js'; //#region Interfaces, Types @@ -523,13 +523,6 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } this._sessions = sessions; - - for (const [resource] of this.sessionStates) { - if (!sessions.has(resource)) { - this.sessionStates.delete(resource); // clean up states for removed sessions - } - } - this._resolved = true; this.logger.logAllStatsIfTrace('Sessions resolved from providers'); @@ -634,7 +627,7 @@ interface ISerializedAgentSession { readonly endTime?: number; }; - readonly changes?: readonly IChatSessionFileChange[] | { + readonly changes?: readonly IChatSessionFileChange[] | readonly IChatSessionFileChange2[] | { readonly files: number; readonly insertions: number; readonly deletions: number; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 2b26ceb4b9168..11c00bc7bda04 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -315,7 +315,7 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre private toDuration(startTime: number, endTime: number, useFullTimeWords: boolean, disallowNow: boolean): string { const elapsed = Math.max(Math.round((endTime - startTime) / 1000) * 1000, 1000 /* clamp to 1s */); - if (!disallowNow && elapsed < 30000) { + if (!disallowNow && elapsed < 60000) { return localize('secondsDuration', "now"); } @@ -331,7 +331,13 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre } if (!timeLabel) { - timeLabel = fromNow(session.timing.lastRequestEnded ?? session.timing.lastRequestStarted ?? session.timing.created, true); + const date = session.timing.lastRequestEnded ?? session.timing.lastRequestStarted ?? session.timing.created; + const seconds = Math.round((new Date().getTime() - date) / 1000); + if (seconds < 60) { + timeLabel = localize('secondsDuration', "now"); + } else { + timeLabel = fromNow(date, true); + } } return timeLabel; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/unifiedQuickAccess.css b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/unifiedQuickAccess.css index d169b0cdb332e..9feca2208721f 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/unifiedQuickAccess.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/unifiedQuickAccess.css @@ -77,7 +77,8 @@ } .unified-send-button .codicon { - font-size: 14px; + font-size: 14px !important; + color: var(--vscode-button-foreground) !important; } .unified-send-label { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index eb15e9e65c5a7..6b3732ca594b5 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -211,6 +211,7 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart private readonly _showOutputAction = this._register(new MutableDisposable()); private _showOutputActionAdded = false; private readonly _focusAction = this._register(new MutableDisposable()); + private readonly _continueInBackgroundAction = this._register(new MutableDisposable()); private readonly _terminalData: IChatTerminalToolInvocationData; private _terminalCommandUri: URI | undefined; @@ -457,6 +458,14 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart }); this._terminalSessionRegistration = this._store.add(listener); } + + // Listen for continue in background to remove the button + this._store.add(this._terminalChatService.onDidContinueInBackground(sessionId => { + if (sessionId === terminalToolSessionId) { + this._terminalData.didContinueInBackground = true; + this._removeContinueInBackgroundAction(); + } + })); } private _addActions(terminalInstance?: ITerminalInstance, terminalToolSessionId?: string): void { @@ -467,11 +476,24 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart this._removeFocusAction(); const resolvedCommand = this._getResolvedCommand(terminalInstance); + this._removeContinueInBackgroundAction(); if (terminalInstance) { const isTerminalHidden = terminalInstance && terminalToolSessionId ? this._terminalChatService.isBackgroundTerminal(terminalToolSessionId) : false; const focusAction = this._instantiationService.createInstance(FocusChatInstanceAction, terminalInstance, resolvedCommand, this._terminalCommandUri, this._storedCommandId, isTerminalHidden); this._focusAction.value = focusAction; actionBar.push(focusAction, { icon: true, label: false, index: 0 }); + + // Add continue in background action - only for foreground executions with running commands + // Note: isBackground refers to whether the tool was invoked with isBackground=true (background execution), + // not whether the terminal is hidden from the user + if (terminalToolSessionId && !this._terminalData.isBackground && !this._terminalData.didContinueInBackground) { + const isStillRunning = resolvedCommand?.exitCode === undefined && this._terminalData.terminalCommandState?.exitCode === undefined; + if (isStillRunning) { + const continueAction = this._instantiationService.createInstance(ContinueInBackgroundAction, terminalToolSessionId); + this._continueInBackgroundAction.value = continueAction; + actionBar.push(continueAction, { icon: true, label: false, index: 0 }); + } + } } this._ensureShowOutputAction(resolvedCommand); @@ -675,6 +697,21 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart this._focusAction.clear(); } + private _removeContinueInBackgroundAction(): void { + if (this._store.isDisposed) { + return; + } + const actionBar = this._actionBar; + const continueAction = this._continueInBackgroundAction.value; + if (actionBar && continueAction) { + const existingIndex = actionBar.viewItems.findIndex(item => item.action === continueAction); + if (existingIndex >= 0) { + actionBar.pull(existingIndex); + } + } + this._continueInBackgroundAction.clear(); + } + private async _toggleOutput(expanded: boolean): Promise { const didChange = await this._outputView.toggle(expanded); const isExpanded = this._outputView.isExpanded; @@ -1450,6 +1487,24 @@ export class FocusChatInstanceAction extends Action implements IAction { } } +export class ContinueInBackgroundAction extends Action implements IAction { + constructor( + private readonly _terminalToolSessionId: string, + @ITerminalChatService private readonly _terminalChatService: ITerminalChatService, + ) { + super( + TerminalContribCommandId.ContinueInBackground, + localize('continueInBackground', 'Continue in Background'), + ThemeIcon.asClassName(Codicon.debugContinue), + true, + ); + } + + public override async run(): Promise { + this._terminalChatService.continueInBackground(this._terminalToolSessionId); + } +} + class ChatTerminalThinkingCollapsibleWrapper extends ChatCollapsibleContentPart { private readonly _terminalContentElement: HTMLElement; private readonly _commandText: string; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index a0fa43226d226..01bcdcf72a334 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -84,7 +84,7 @@ import { IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from import { IChatModelInputState, IChatRequestModeInfo, IInputModel } from '../../../common/model/chatModel.js'; import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js'; import { IChatFollowup, IChatService, IChatSessionContext } from '../../../common/chatService/chatService.js'; -import { agentOptionId, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; +import { agentOptionId, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsService, isIChatSessionFileChange2, localChatSessionType } from '../../../common/chatSessionsService.js'; import { getChatSessionType } from '../../../common/model/chatUri.js'; import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringImplicitContextValue, isStringVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; import { IChatResponseViewModel } from '../../../common/model/chatViewModel.js'; @@ -2093,7 +2093,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const newHeight = this.container.offsetHeight; this.height.set(newHeight, undefined); })); - inputResizeObserver.observe(this.container); + this._register(inputResizeObserver.observe(this.container)); } public toggleChatInputOverlay(editing: boolean): void { @@ -2428,7 +2428,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const sessionFiles = derived(reader => sessionFileChanges.read(reader).map((entry): IChatCollapsibleListItem => ({ - reference: entry.modifiedUri, + reference: isIChatSessionFileChange2(entry) ? entry.uri : entry.modifiedUri, state: ModifiedFileEntryState.Accepted, kind: 'reference', options: { diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 5df7c58f631a2..3d04fb6169b23 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -416,6 +416,8 @@ export interface IChatTerminalToolInvocationData { terminalToolSessionId?: string; /** The predefined command ID that will be used for this terminal command */ terminalCommandId?: string; + /** Whether the terminal command was started as a background execution */ + isBackground?: boolean; /** Serialized URI for the command that was executed in the terminal */ terminalCommandUri?: UriComponents; /** Serialized output of the executed command */ @@ -435,6 +437,8 @@ export interface IChatTerminalToolInvocationData { timestamp?: number; duration?: number; }; + /** Whether the user chose to continue in background for this tool invocation */ + didContinueInBackground?: boolean; autoApproveInfo?: IMarkdownString; } diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index e0dacd3f5f47c..27ff794566e22 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -94,7 +94,7 @@ export interface IChatSessionItem { files: number; insertions: number; deletions: number; - } | readonly IChatSessionFileChange[]; + } | readonly IChatSessionFileChange[] | readonly IChatSessionFileChange2[]; archived?: boolean; } @@ -105,6 +105,14 @@ export interface IChatSessionFileChange { deletions: number; } +export interface IChatSessionFileChange2 { + readonly uri: URI; + readonly originalUri?: URI; + readonly modifiedUri?: URI; + readonly insertions: number; + readonly deletions: number; +} + export type IChatSessionHistoryItem = { id?: string; type: 'request'; @@ -255,4 +263,9 @@ export function isSessionInProgressStatus(state: ChatSessionStatus): boolean { return state === ChatSessionStatus.InProgress || state === ChatSessionStatus.NeedsInput; } +export function isIChatSessionFileChange2(obj: unknown): obj is IChatSessionFileChange2 { + const candidate = obj as IChatSessionFileChange2; + return candidate && candidate.uri instanceof URI && typeof candidate.insertions === 'number' && typeof candidate.deletions === 'number'; +} + export const IChatSessionsService = createDecorator('chatSessionsService'); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 6e0c2c0ff5266..25d30cd3700c1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -56,6 +56,7 @@ export class StartSessionAction extends Action2 { super({ id: ACTION_START, title: localize2('run', 'Open Inline Chat'), + shortTitle: localize2('runShort', 'Inline Chat'), category: AbstractInlineChatAction.category, f1: true, precondition: inlineChatContextKey, @@ -78,6 +79,10 @@ export class StartSessionAction extends Action2 { id: MenuId.ChatEditorInlineGutter, group: '1_chat', order: 1, + }, { + id: MenuId.InlineChatEditorAffordance, + group: '1_chat', + order: 1, }] }); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts index 3fe2e7636c93b..b7e09ff6fb7d4 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts @@ -6,14 +6,14 @@ import './media/inlineChatEditorAffordance.css'; import { IDimension } from '../../../../base/browser/dom.js'; import * as dom from '../../../../base/browser/dom.js'; -import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; import { autorun, IObservable, ISettableObservable } from '../../../../base/common/observable.js'; -import { assertType } from '../../../../base/common/types.js'; +import { MenuId } from '../../../../platform/actions/common/actions.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; /** * Content widget that shows a small sparkle icon at the cursor position. @@ -35,22 +35,20 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi private readonly _editor: ICodeEditor, selection: IObservable, suppressAffordance: ISettableObservable, - private readonly _hover: ISettableObservable<{ rect: DOMRect; above: boolean; lineNumber: number } | undefined> + _hover: ISettableObservable<{ rect: DOMRect; above: boolean; lineNumber: number } | undefined>, + @IInstantiationService instantiationService: IInstantiationService, ) { super(); // Create the widget DOM this._domNode = dom.$('.inline-chat-content-widget'); - // Add sparkle icon - const icon = dom.append(this._domNode, dom.$('.icon')); - icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.sparkleFilled)); - - // Handle click to show overlay widget - this._store.add(dom.addDisposableListener(this._domNode, dom.EventType.CLICK, (e) => { - e.preventDefault(); - e.stopPropagation(); - this._showOverlayWidget(); + // Create toolbar with the inline chat start action + this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._domNode, MenuId.InlineChatEditorAffordance, { + telemetrySource: 'inlineChatEditorAffordance', + hiddenItemStrategy: HiddenItemStrategy.Ignore, + menuOptions: { renderShortTitle: true }, + toolbarOptions: { primaryGroup: () => true }, })); this._store.add(autorun(r => { @@ -95,28 +93,6 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi } } - private _showOverlayWidget(): void { - assertType(this._editor.hasModel()); - - if (!this._position || !this._position.position) { - return; - } - - const position = this._position.position; - const editorDomNode = this._editor.getDomNode(); - const scrolledPosition = this._editor.getScrolledVisiblePosition(position); - const editorRect = editorDomNode.getBoundingClientRect(); - const x = editorRect.left + scrolledPosition.left; - const y = editorRect.top + scrolledPosition.top; - - this._hide(); - this._hover.set({ - rect: new DOMRect(x, y, 0, scrolledPosition.height), - above: this._position.preference[0] === ContentWidgetPositionPreference.ABOVE, - lineNumber: position.lineNumber - }, undefined); - } - getId(): string { return this._id; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 0f85cfc076a30..84e873083dcd6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -86,7 +86,7 @@ export class InlineChatInputWidget extends Disposable { // Create editor options const options = getSimpleEditorOptions(configurationService); - options.wordWrap = 'off'; + options.wordWrap = 'on'; options.lineNumbers = 'off'; options.glyphMargin = false; options.lineDecorationsWidth = 0; @@ -95,7 +95,6 @@ export class InlineChatInputWidget extends Disposable { options.minimap = { enabled: false }; options.scrollbar = { vertical: 'auto', horizontal: 'hidden', alwaysConsumeMouseWheel: true, verticalSliderSize: 6 }; options.renderLineHighlight = 'none'; - options.placeholder = this._keybindingService.appendKeybinding(localize('placeholderWithSelection', "Edit selection"), ACTION_START); const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { isSimpleWidget: true, @@ -110,6 +109,18 @@ export class InlineChatInputWidget extends Disposable { this._input.setModel(model); this._input.layout({ width: 200, height: 18 }); + // Update placeholder based on selection state + this._store.add(autorun(r => { + const selection = this._editorObs.cursorSelection.read(r); + const hasSelection = selection && !selection.isEmpty(); + const placeholderText = hasSelection + ? localize('placeholderWithSelection', "Edit selection") + : localize('placeholderNoSelection', "Generate code"); + this._input.updateOptions({ + placeholder: this._keybindingService.appendKeybinding(placeholderText, ACTION_START) + }); + })); + // Listen to content size changes and resize the input editor (max 3 lines) this._store.add(this._input.onDidContentSizeChange(e => { if (e.contentHeightChanged) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index e970952c6193f..842572dbad9cb 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -324,23 +324,23 @@ } .inline-chat-gutter-menu .input { - padding: 0 18px; + padding: 0 8px; } .inline-chat-gutter-menu .monaco-action-bar.vertical .action-item { display: flex; justify-content: space-between; - padding: 0 .8em; border-radius: 3px; margin: 0 4px; } .inline-chat-gutter-menu .monaco-action-bar.vertical .action-item .action-label { font-size: 13px; + width: 100%; } .inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):hover, -.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled).focused { +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):focus-within { background-color: var(--vscode-list-activeSelectionBackground); color: var(--vscode-list-activeSelectionForeground); outline: 1px solid var(--vscode-menu-selectionBorder, transparent); @@ -348,6 +348,8 @@ } .inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):hover .action-label, -.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled).focused .action-label { +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):focus-within .action-label { color: var(--vscode-list-activeSelectionForeground); + outline: 1px solid var(--vscode-menu-selectionBorder, transparent); + outline-offset: -1px; } diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index 814a993312ee2..e493fe31c9247 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -27,6 +27,7 @@ left: 0; width: 16px; height: 100%; + border-radius: var(--vscode-cornerRadius-small); color: inherit; box-sizing: border-box; background-position: center center; diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index d5ace8862c6a1..64e0df0108a19 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -239,6 +239,18 @@ export interface ITerminalChatService { * @returns A record of all session-scoped auto-approve rules for the session */ getSessionAutoApproveRules(chatSessionResource: URI): Readonly>; + + /** + * Signal that a foreground terminal tool invocation should continue in the background. + * This causes the tool to return its current output immediately while the terminal keeps running. + * @param terminalToolSessionId The tool session ID to continue in background + */ + continueInBackground(terminalToolSessionId: string): void; + + /** + * Event fired when a terminal tool invocation should continue in the background. + */ + readonly onDidContinueInBackground: Event; } /** diff --git a/src/vs/workbench/contrib/terminal/terminalContribExports.ts b/src/vs/workbench/contrib/terminal/terminalContribExports.ts index 1fd3bb600a07d..5692332ebc9eb 100644 --- a/src/vs/workbench/contrib/terminal/terminalContribExports.ts +++ b/src/vs/workbench/contrib/terminal/terminalContribExports.ts @@ -32,6 +32,7 @@ export const enum TerminalContribCommandId { FocusMostRecentChatTerminal = TerminalChatCommandId.FocusMostRecentChatTerminal, ToggleChatTerminalOutput = TerminalChatCommandId.ToggleChatTerminalOutput, FocusChatInstanceAction = TerminalChatCommandId.FocusChatInstanceAction, + ContinueInBackground = TerminalChatCommandId.ContinueInBackground, } // HACK: Export some settings from `terminalContrib/` that are depended upon elsewhere. These are diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index 44561ce3ae518..bb5f43232886c 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -25,6 +25,7 @@ export const enum TerminalChatCommandId { FocusMostRecentChatTerminal = 'workbench.action.terminal.chat.focusMostRecentChatTerminal', ToggleChatTerminalOutput = 'workbench.action.terminal.chat.toggleChatTerminalOutput', FocusChatInstanceAction = 'workbench.action.terminal.chat.focusChatInstance', + ContinueInBackground = 'workbench.action.terminal.chat.continueInBackground', } export const MENU_TERMINAL_CHAT_WIDGET_INPUT_SIDE_TOOLBAR = MenuId.for('terminalChatWidget'); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts index 936f48a8d87f5..e815d4e93cb81 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts @@ -33,8 +33,12 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ private readonly _chatSessionResourceByTerminalInstance = new Map(); private readonly _terminalInstanceListenersByToolSessionId = this._register(new DisposableMap()); private readonly _chatSessionListenersByTerminalInstance = this._register(new DisposableMap()); - private readonly _onDidRegisterTerminalInstanceForToolSession = new Emitter(); + + private readonly _onDidContinueInBackground = this._register(new Emitter()); + readonly onDidContinueInBackground: Event = this._onDidContinueInBackground.event; + private readonly _onDidRegisterTerminalInstanceForToolSession = this._register(new Emitter()); readonly onDidRegisterTerminalInstanceWithToolSession: Event = this._onDidRegisterTerminalInstanceForToolSession.event; + private readonly _activeProgressParts = new Set(); private _focusedProgressPart: IChatTerminalToolProgressPart | undefined; private _mostRecentProgressPart: IChatTerminalToolProgressPart | undefined; @@ -345,4 +349,8 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ getSessionAutoApproveRules(chatSessionResource: URI): Readonly> { return this._sessionAutoApproveRules.get(chatSessionResource) ?? {}; } + + continueInBackground(terminalToolSessionId: string): void { + this._onDidContinueInBackground.fire(terminalToolSessionId); + } } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index 6bbe90a640c1d..45aa0be265b1a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -427,6 +427,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { }, cwd, language, + isBackground: args.isBackground, }; // HACK: Exit early if there's an alternative recommendation, this is a little hacky but @@ -676,12 +677,13 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { inputUserSigint ||= data === '\x03'; })); + let didMoveToBackground = false; let outputMonitor: OutputMonitor | undefined; if (args.isBackground) { let pollingResult: IPollingResult & { pollDurationMs: number } | undefined; try { this._logService.debug(`RunInTerminalTool: Starting background execution \`${command}\``); - const execution = new BackgroundTerminalExecution(toolTerminal.instance, xterm, command, chatSessionId, commandId); + const execution = BackgroundTerminalExecution.start(toolTerminal.instance, xterm, command, chatSessionId, commandId!); RunInTerminalTool._backgroundExecutions.set(termId, execution); outputMonitor = store.add(this._instantiationService.createInstance(OutputMonitor, execution, undefined, invocation.context!, token, command)); @@ -763,6 +765,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { let exitCode: number | undefined; let altBufferResult: IToolResult | undefined; let didTimeout = false; + let didContinueInBackground = false; let timeoutPromise: CancelablePromise | undefined; const executeCancellation = store.add(new CancellationTokenSource(token)); @@ -780,15 +783,33 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } } + // Set up continue in background listener + if (terminalToolSessionId) { + store.add(this._terminalChatService.onDidContinueInBackground(sessionId => { + if (sessionId === terminalToolSessionId) { + didContinueInBackground = true; + if (!executeCancellation.token.isCancellationRequested) { + executeCancellation.cancel(); + } + } + })); + } + + let startMarker: IXtermMarker | undefined; try { const strategy: ITerminalExecuteStrategy = this._getExecuteStrategy(toolTerminal.shellIntegrationQuality, toolTerminal, commandDetection!); if (toolTerminal.shellIntegrationQuality === ShellIntegrationQuality.None) { toolResultMessage = '$(info) Enable [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) to improve command detection'; } this._logService.debug(`RunInTerminalTool: Using \`${strategy.type}\` execute strategy for command \`${command}\``); - store.add(strategy.onDidCreateStartMarker(startMarker => { + store.add(strategy.onDidCreateStartMarker(startMarker_ => { + if (startMarker_) { + // Duplicate marker for use even if the execution strategy one gets disposed. + // Don't add to store - we may need to transfer ownership to BackgroundTerminalExecution. + startMarker = xterm.raw.registerMarker(startMarker_.line - (xterm.raw.buffer.active.cursorY + xterm.raw.buffer.active.baseY)); + } if (!outputMonitor) { - outputMonitor = store.add(this._instantiationService.createInstance(OutputMonitor, { instance: toolTerminal.instance, sessionId: invocation.context?.sessionId, getOutput: (marker?: IXtermMarker) => getOutput(toolTerminal.instance, marker ?? startMarker) }, undefined, invocation.context, token, command)); + outputMonitor = store.add(this._instantiationService.createInstance(OutputMonitor, { instance: toolTerminal.instance, sessionId: invocation.context?.sessionId, getOutput: (marker?: IXtermMarker) => getOutput(toolTerminal.instance, marker ?? startMarker_) }, undefined, invocation.context, token, command)); } })); const executeResult = await strategy.execute(command, executeCancellation.token, commandId); @@ -851,6 +872,21 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { const timeoutOutput = getOutput(toolTerminal.instance, undefined); outputLineCount = timeoutOutput ? count(timeoutOutput.trim(), '\n') + 1 : 0; terminalResult = timeoutOutput ?? ''; + } else if (didContinueInBackground && e instanceof CancellationError) { + // Handle continue in background case - get output collected so far and return it + this._logService.debug(`RunInTerminalTool: Continue in background triggered, returning output collected so far`); + error = 'continueInBackground'; + didMoveToBackground = true; + if (!startMarker) { + this._logService.debug(`RunInTerminalTool: Start marker is undefined`); + } else if (startMarker.isDisposed) { + this._logService.debug(`RunInTerminalTool: Start marker is disposed`); + } + const execution = BackgroundTerminalExecution.adopt(toolTerminal.instance, xterm, command, chatSessionId, startMarker); + RunInTerminalTool._backgroundExecutions.set(termId, execution); + const backgroundOutput = execution.getOutput(); + outputLineCount = backgroundOutput ? count(backgroundOutput.trim(), '\n') + 1 : 0; + terminalResult = backgroundOutput ?? ''; } else { this._logService.debug(`RunInTerminalTool: Threw exception`); toolTerminal.instance.dispose(); @@ -860,6 +896,10 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } finally { timeoutPromise?.cancel(); store.dispose(); + // Clean up the marker if we didn't move to background (which takes ownership) + if (!didMoveToBackground) { + startMarker?.dispose(); + } const timingExecuteMs = Date.now() - timingStart; this._telemetry.logInvoke(toolTerminal.instance, { terminalToolSessionId: toolSpecificData.terminalToolSessionId, @@ -898,6 +938,9 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } else if (didToolEditCommand) { resultText.push(`Note: The tool simplified the command to \`${command}\`, and this is the output of running that command instead:\n`); } + if (didMoveToBackground) { + resultText.push(`Note: This terminal execution was moved to the background using the ID ${termId}\n`); + } resultText.push(terminalResult); return { @@ -1107,7 +1150,31 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { class BackgroundTerminalExecution extends Disposable { private _startMarker?: IXtermMarker; - constructor( + static start( + instance: ITerminalInstance, + xterm: XtermTerminal, + commandLine: string, + sessionId: string, + commandId: string, + ): BackgroundTerminalExecution { + return new BackgroundTerminalExecution(instance, xterm, commandLine, sessionId, commandId); + } + + static adopt( + instance: ITerminalInstance, + xterm: XtermTerminal, + commandLine: string, + sessionId: string, + existingMarker?: IXtermMarker, + ): BackgroundTerminalExecution { + const execution = new BackgroundTerminalExecution(instance, xterm, commandLine, sessionId); + if (existingMarker) { + execution._startMarker = execution._register(existingMarker); + } + return execution; + } + + private constructor( readonly instance: ITerminalInstance, private readonly _xterm: XtermTerminal, private readonly _commandLine: string, @@ -1116,9 +1183,12 @@ class BackgroundTerminalExecution extends Disposable { ) { super(); - this._startMarker = this._register(this._xterm.raw.registerMarker()); - this.instance.runCommand(this._commandLine, true, commandId); + if (commandId !== undefined) { + this._startMarker = this._register(this._xterm.raw.registerMarker()); + this.instance.runCommand(this._commandLine, true, commandId); + } } + getOutput(marker?: IXtermMarker): string { return getOutput(this.instance, marker ?? this._startMarker); } diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index cc12ca62fbb67..a7450ed69427d 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -12,8 +12,8 @@ import { IInstantiationService, ServicesAccessor } from '../../../../platform/in import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { IUpdateService, State as UpdateState, StateType, IUpdate, DisablementReason } from '../../../../platform/update/common/update.js'; -import { INotificationService, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js'; +import { IUpdateService, State as UpdateState, StateType, IUpdate, DisablementReason, Ready, Overwriting } from '../../../../platform/update/common/update.js'; +import { INotificationService, INotificationHandle, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IBrowserWorkbenchEnvironmentService } from '../../../services/environment/browser/environmentService.js'; import { ReleaseNotesManager } from './releaseNotesEditor.js'; @@ -161,6 +161,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu private state: UpdateState; private readonly badgeDisposable = this._register(new MutableDisposable()); + private overwriteNotificationHandle: INotificationHandle | undefined; private updateStateContextKey: IContextKey; private majorMinorUpdateAvailableContextKey: IContextKey; @@ -244,14 +245,18 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu this.onUpdateDownloaded(state.update); break; + case StateType.Overwriting: + this.onUpdateOverwriting(state); + break; + case StateType.Ready: { const productVersion = state.update.productVersion; if (productVersion) { const currentVersion = parseVersion(this.productService.version); const nextVersion = parseVersion(productVersion); this.majorMinorUpdateAvailableContextKey.set(Boolean(currentVersion && nextVersion && isMajorMinorUpdate(currentVersion, nextVersion))); - this.onUpdateReady(state.update); } + this.onUpdateReady(state); break; } } @@ -262,7 +267,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", this.productService.nameShort)); } else if (state.type === StateType.CheckingForUpdates) { badge = new ProgressBadge(() => nls.localize('checkingForUpdates', "Checking for {0} updates...", this.productService.nameShort)); - } else if (state.type === StateType.Downloading) { + } else if (state.type === StateType.Downloading || state.type === StateType.Overwriting) { badge = new ProgressBadge(() => nls.localize('downloading', "Downloading {0} update...", this.productService.nameShort)); } else if (state.type === StateType.Updating) { badge = new ProgressBadge(() => nls.localize('updating', "Updating {0}...", this.productService.nameShort)); @@ -363,39 +368,71 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu } // windows and mac - private onUpdateReady(update: IUpdate): void { - if (!(isWindows && this.productService.target !== 'user') && !this.shouldShowNotification()) { - return; - } + private onUpdateReady(state: Ready): void { + if (state.overwrite && this.overwriteNotificationHandle) { + const handle = this.overwriteNotificationHandle; + this.overwriteNotificationHandle = undefined; + + // Update notification to show completion with restart action + handle.progress.done(); + handle.updateMessage(nls.localize('newerUpdateReady', "The newer update is ready to install.")); + handle.updateActions({ + primary: [ + toAction({ + id: 'update.restartToUpdate', + label: nls.localize('restartToUpdate2', "Restart to Update"), + run: () => this.updateService.quitAndInstall() + }) + ] + }); + } else if ((isWindows && this.productService.target !== 'user') || this.shouldShowNotification()) { - const actions = [{ - label: nls.localize('updateNow', "Update Now"), - run: () => this.updateService.quitAndInstall() - }, { - label: nls.localize('later', "Later"), - run: () => { } - }]; + const actions = [{ + label: nls.localize('updateNow', "Update Now"), + run: () => this.updateService.quitAndInstall() + }, { + label: nls.localize('later', "Later"), + run: () => { } + }]; + + const productVersion = state.update.productVersion; + if (productVersion) { + actions.push({ + label: nls.localize('releaseNotes', "Release Notes"), + run: () => { + this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); + } + }); + } - const productVersion = update.productVersion; - if (productVersion) { - actions.push({ - label: nls.localize('releaseNotes', "Release Notes"), - run: () => { - this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); + // windows user fast updates and mac + this.notificationService.prompt( + severity.Info, + nls.localize('updateAvailableAfterRestart', "Restart {0} to apply the latest update.", this.productService.nameLong), + actions, + { + sticky: true, + priority: NotificationPriority.OPTIONAL } - }); + ); } + } - // windows user fast updates and mac - this.notificationService.prompt( - severity.Info, - nls.localize('updateAvailableAfterRestart', "Restart {0} to apply the latest update.", this.productService.nameLong), - actions, - { - sticky: true, - priority: NotificationPriority.OPTIONAL - } - ); + // macOS overwrite update - overwriting + private onUpdateOverwriting(state: Overwriting): void { + if (!state.explicit) { + return; + } + + // Show notification with progress + this.overwriteNotificationHandle = this.notificationService.notify({ + severity: Severity.Info, + sticky: true, + message: nls.localize('newerUpdateDownloading', "We found a newer update available and have started to download it. We'll let you know as soon as it's ready to install."), + source: nls.localize('update service', "Update Service"), + }); + + this.overwriteNotificationHandle.progress.infinite(); } private shouldShowNotification(): boolean { diff --git a/src/vs/workbench/services/update/browser/updateService.ts b/src/vs/workbench/services/update/browser/updateService.ts index 65016e9067b6c..cced4f2d1838b 100644 --- a/src/vs/workbench/services/update/browser/updateService.ts +++ b/src/vs/workbench/services/update/browser/updateService.ts @@ -70,7 +70,7 @@ export class BrowserUpdateService extends Disposable implements IUpdateService { const update = await updateProvider.checkForUpdate(); if (update) { // State -> Downloaded - this.state = State.Ready({ version: update.version, productVersion: update.version }); + this.state = State.Ready({ version: update.version, productVersion: update.version }, explicit, false); } else { // State -> Idle this.state = State.Idle(UpdateType.Archive); diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index 3f8a6a0b89f44..7d07be4b52bb3 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -231,7 +231,7 @@ declare module 'vscode' { /** * Statistics about the chat session. */ - changes?: readonly ChatSessionChangedFile[] | { + changes?: readonly ChatSessionChangedFile[] | readonly ChatSessionChangedFile2[] | { /** * Number of files edited during the session. */ @@ -273,6 +273,35 @@ declare module 'vscode' { constructor(modifiedUri: Uri, insertions: number, deletions: number, originalUri?: Uri); } + export class ChatSessionChangedFile2 { + /** + * URI of the file. + */ + readonly uri: Uri; + + /** + * URI of the original file. Undefined if the file was created. + */ + readonly originalUri: Uri | undefined; + + /** + * URI of the modified file. Undefined if the file was deleted. + */ + readonly modifiedUri: Uri | undefined; + + /** + * Number of insertions made during the session. + */ + insertions: number; + + /** + * Number of deletions made during the session. + */ + deletions: number; + + constructor(uri: Uri, originalUri: Uri | undefined, modifiedUri: Uri | undefined, insertions: number, deletions: number); + } + export interface ChatSession { /** * The full history of the session diff --git a/test/monaco/package-lock.json b/test/monaco/package-lock.json index ada3052b87ee2..513d33eeb3444 100644 --- a/test/monaco/package-lock.json +++ b/test/monaco/package-lock.json @@ -144,9 +144,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" },