diff --git a/extensions/extension-editing/src/extensionEditingBrowserMain.ts b/extensions/extension-editing/src/extensionEditingBrowserMain.ts index f9d6885c6223c..57c969d017020 100644 --- a/extensions/extension-editing/src/extensionEditingBrowserMain.ts +++ b/extensions/extension-editing/src/extensionEditingBrowserMain.ts @@ -5,11 +5,14 @@ import * as vscode from 'vscode'; import { PackageDocument } from './packageDocumentHelper'; +import { PackageDocumentL10nSupport } from './packageDocumentL10nSupport'; export function activate(context: vscode.ExtensionContext) { //package.json suggestions context.subscriptions.push(registerPackageDocumentCompletions()); + //package.json go to definition for NLS strings + context.subscriptions.push(new PackageDocumentL10nSupport()); } function registerPackageDocumentCompletions(): vscode.Disposable { @@ -18,5 +21,4 @@ function registerPackageDocumentCompletions(): vscode.Disposable { return new PackageDocument(document).provideCompletionItems(position, token); } }); - } diff --git a/extensions/extension-editing/src/extensionEditingMain.ts b/extensions/extension-editing/src/extensionEditingMain.ts index c056fbfa975ae..c620b3039541f 100644 --- a/extensions/extension-editing/src/extensionEditingMain.ts +++ b/extensions/extension-editing/src/extensionEditingMain.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import { PackageDocument } from './packageDocumentHelper'; +import { PackageDocumentL10nSupport } from './packageDocumentL10nSupport'; import { ExtensionLinter } from './extensionLinter'; export function activate(context: vscode.ExtensionContext) { @@ -15,6 +16,9 @@ export function activate(context: vscode.ExtensionContext) { //package.json code actions for lint warnings context.subscriptions.push(registerCodeActionsProvider()); + // package.json l10n support + context.subscriptions.push(new PackageDocumentL10nSupport()); + context.subscriptions.push(new ExtensionLinter()); } diff --git a/extensions/extension-editing/src/packageDocumentL10nSupport.ts b/extensions/extension-editing/src/packageDocumentL10nSupport.ts new file mode 100644 index 0000000000000..4d844e98d5f71 --- /dev/null +++ b/extensions/extension-editing/src/packageDocumentL10nSupport.ts @@ -0,0 +1,204 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { getLocation, getNodeValue, parseTree, findNodeAtLocation, visit } from 'jsonc-parser'; + + +const packageJsonSelector: vscode.DocumentSelector = { language: 'json', pattern: '**/package.json' }; +const packageNlsJsonSelector: vscode.DocumentSelector = { language: 'json', pattern: '**/package.nls.json' }; + +export class PackageDocumentL10nSupport implements vscode.DefinitionProvider, vscode.ReferenceProvider, vscode.Disposable { + + private readonly _disposables: vscode.Disposable[] = []; + + constructor() { + this._disposables.push(vscode.languages.registerDefinitionProvider(packageJsonSelector, this)); + this._disposables.push(vscode.languages.registerDefinitionProvider(packageNlsJsonSelector, this)); + + this._disposables.push(vscode.languages.registerReferenceProvider(packageNlsJsonSelector, this)); + this._disposables.push(vscode.languages.registerReferenceProvider(packageJsonSelector, this)); + } + + dispose(): void { + for (const d of this._disposables) { + d.dispose(); + } + } + + public async provideDefinition(document: vscode.TextDocument, position: vscode.Position, _token: vscode.CancellationToken): Promise { + const basename = document.uri.path.split('/').pop()?.toLowerCase(); + if (basename === 'package.json') { + return this.provideNlsValueDefinition(document, position); + } + + if (basename === 'package.nls.json') { + return this.provideNlsKeyDefinition(document, position); + } + + return undefined; + } + + private async provideNlsValueDefinition(packageJsonDoc: vscode.TextDocument, position: vscode.Position): Promise { + const nlsRef = this.getNlsReferenceAtPosition(packageJsonDoc, position); + if (!nlsRef) { + return undefined; + } + + const nlsUri = vscode.Uri.joinPath(packageJsonDoc.uri, '..', 'package.nls.json'); + return this.resolveNlsDefinition(nlsRef, nlsUri); + } + + private async provideNlsKeyDefinition(nlsDoc: vscode.TextDocument, position: vscode.Position): Promise { + const nlsKey = this.getNlsKeyDefinitionAtPosition(nlsDoc, position); + if (!nlsKey) { + return undefined; + } + return this.resolveNlsDefinition(nlsKey, nlsDoc.uri); + } + + private async resolveNlsDefinition(origin: { key: string; range: vscode.Range }, nlsUri: vscode.Uri): Promise { + const target = await this.findNlsKeyDeclaration(origin.key, nlsUri); + if (!target) { + return undefined; + } + + return [{ + originSelectionRange: origin.range, + targetUri: target.uri, + targetRange: target.range, + }]; + } + + private getNlsReferenceAtPosition(packageJsonDoc: vscode.TextDocument, position: vscode.Position): { key: string; range: vscode.Range } | undefined { + const location = getLocation(packageJsonDoc.getText(), packageJsonDoc.offsetAt(position)); + if (!location.previousNode || location.previousNode.type !== 'string') { + return undefined; + } + + const value = getNodeValue(location.previousNode); + if (typeof value !== 'string') { + return undefined; + } + + const match = value.match(/^%(.+)%$/); + if (!match) { + return undefined; + } + + const nodeStart = packageJsonDoc.positionAt(location.previousNode.offset); + const nodeEnd = packageJsonDoc.positionAt(location.previousNode.offset + location.previousNode.length); + return { key: match[1], range: new vscode.Range(nodeStart, nodeEnd) }; + } + + public async provideReferences(document: vscode.TextDocument, position: vscode.Position, context: vscode.ReferenceContext, _token: vscode.CancellationToken): Promise { + const basename = document.uri.path.split('/').pop()?.toLowerCase(); + if (basename === 'package.nls.json') { + return this.provideNlsKeyReferences(document, position, context); + } + if (basename === 'package.json') { + return this.provideNlsValueReferences(document, position, context); + } + return undefined; + } + + private async provideNlsKeyReferences(nlsDoc: vscode.TextDocument, position: vscode.Position, context: vscode.ReferenceContext): Promise { + const nlsKey = this.getNlsKeyDefinitionAtPosition(nlsDoc, position); + if (!nlsKey) { + return undefined; + } + + const packageJsonUri = vscode.Uri.joinPath(nlsDoc.uri, '..', 'package.json'); + return this.findAllNlsReferences(nlsKey.key, packageJsonUri, nlsDoc.uri, context); + } + + private async provideNlsValueReferences(packageJsonDoc: vscode.TextDocument, position: vscode.Position, context: vscode.ReferenceContext): Promise { + const nlsRef = this.getNlsReferenceAtPosition(packageJsonDoc, position); + if (!nlsRef) { + return undefined; + } + + const nlsUri = vscode.Uri.joinPath(packageJsonDoc.uri, '..', 'package.nls.json'); + return this.findAllNlsReferences(nlsRef.key, packageJsonDoc.uri, nlsUri, context); + } + + private async findAllNlsReferences(nlsKey: string, packageJsonUri: vscode.Uri, nlsUri: vscode.Uri, context: vscode.ReferenceContext): Promise { + const locations = await this.findNlsReferencesInPackageJson(nlsKey, packageJsonUri); + + if (context.includeDeclaration) { + const decl = await this.findNlsKeyDeclaration(nlsKey, nlsUri); + if (decl) { + locations.push(decl); + } + } + + return locations; + } + + private async findNlsKeyDeclaration(nlsKey: string, nlsUri: vscode.Uri): Promise { + try { + const nlsDoc = await vscode.workspace.openTextDocument(nlsUri); + const nlsTree = parseTree(nlsDoc.getText()); + if (!nlsTree) { + return undefined; + } + + const node = findNodeAtLocation(nlsTree, [nlsKey]); + if (!node?.parent) { + return undefined; + } + + const keyNode = node.parent.children?.[0]; + if (!keyNode) { + return undefined; + } + + const start = nlsDoc.positionAt(keyNode.offset); + const end = nlsDoc.positionAt(keyNode.offset + keyNode.length); + return new vscode.Location(nlsUri, new vscode.Range(start, end)); + } catch { + return undefined; + } + } + + private async findNlsReferencesInPackageJson(nlsKey: string, packageJsonUri: vscode.Uri): Promise { + let packageJsonDoc: vscode.TextDocument; + try { + packageJsonDoc = await vscode.workspace.openTextDocument(packageJsonUri); + } catch { + return []; + } + + const text = packageJsonDoc.getText(); + const needle = `%${nlsKey}%`; + const locations: vscode.Location[] = []; + + visit(text, { + onLiteralValue(value, offset, length) { + if (value === needle) { + const start = packageJsonDoc.positionAt(offset); + const end = packageJsonDoc.positionAt(offset + length); + locations.push(new vscode.Location(packageJsonUri, new vscode.Range(start, end))); + } + } + }); + + return locations; + } + + private getNlsKeyDefinitionAtPosition(nlsDoc: vscode.TextDocument, position: vscode.Position): { key: string; range: vscode.Range } | undefined { + const location = getLocation(nlsDoc.getText(), nlsDoc.offsetAt(position)); + + // Must be on a top-level property key + if (location.path.length !== 1 || !location.isAtPropertyKey || !location.previousNode) { + return undefined; + } + + const key = location.path[0] as string; + const start = nlsDoc.positionAt(location.previousNode.offset); + const end = nlsDoc.positionAt(location.previousNode.offset + location.previousNode.length); + return { key, range: new vscode.Range(start, end) }; + } +} diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 8d3f082600d84..f1b217f5cca39 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -227,7 +227,7 @@ "quickInputList.focusBackground": "#3994BC26", "quickInputList.focusForeground": "#bfbfbf", "quickInputList.focusIconForeground": "#bfbfbf", - "quickInputList.hoverBackground": "#515253", + "quickInputList.hoverBackground": "#262728", "terminal.selectionBackground": "#3994BC33", "terminal.background": "#191A1B", "terminal.border": "#2A2B2CFF", diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index c743617242634..9a3a44fb872c3 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -208,7 +208,7 @@ "breadcrumb.background": "#FFFFFF", "breadcrumb.focusForeground": "#202020", "breadcrumb.activeSelectionForeground": "#202020", - "breadcrumbPicker.background": "#F0F0F3", + "breadcrumbPicker.background": "#FAFAFD", "notificationCenter.border": "#F0F1F2FF", "notificationCenterHeader.foreground": "#202020", "notificationCenterHeader.background": "#FAFAFD", @@ -229,7 +229,7 @@ "extensionButton.prominentHoverBackground": "#0064CC", "pickerGroup.border": "#EEEEF1", "pickerGroup.foreground": "#202020", - "quickInput.background": "#F0F0F3", + "quickInput.background": "#FAFAFD", "quickInput.foreground": "#202020", "quickInputList.focusBackground": "#0069CC1A", "quickInputList.focusForeground": "#202020", @@ -256,7 +256,7 @@ "gauge.errorForeground": "#ad0707", "gauge.errorBackground": "#ad070740", "statusBarItem.prominentHoverForeground": "#FFFFFF", - "quickInputTitle.background": "#F0F0F3", + "quickInputTitle.background": "#FAFAFD", "chat.requestBubbleBackground": "#EEF4FB", "chat.requestBubbleHoverBackground": "#E6EDFA", "chat.thinkingShimmer": "#999999", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 4cdefd6e1dbcd..87790e5e7eba9 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -5,7 +5,6 @@ :root { --radius-sm: 4px; - --radius-md: 6px; --radius-lg: 8px; --shadow-sm: 0 0 4px rgba(0, 0, 0, 0.08); @@ -19,16 +18,10 @@ /* Panel depth shadows cast onto the editor surface */ --shadow-depth-x: 5px 0 10px -4px rgba(0, 0, 0, 0.05); --shadow-depth-y: 0 5px 10px -4px rgba(0, 0, 0, 0.04); - - --backdrop-blur-md: blur(20px) saturate(180%); - --backdrop-blur-lg: blur(40px) saturate(180%); } /* Dark theme: add brightness reduction for contrast-safe luminosity blending over bright backgrounds */ .monaco-workbench.vs-dark { - --backdrop-blur-md: blur(20px) saturate(180%) brightness(0.55); - --backdrop-blur-lg: blur(40px) saturate(180%) brightness(0.55); - --shadow-depth-x: 5px 0 12px -4px rgba(0, 0, 0, 0.14); --shadow-depth-y: 0 5px 12px -4px rgba(0, 0, 0, 0.10); } @@ -125,18 +118,6 @@ /* Quick Input (Command Palette) */ .monaco-workbench .quick-input-widget { box-shadow: var(--shadow-xl) !important; - border-radius: 12px; - background-color: color-mix(in srgb, var(--vscode-quickInput-background) 60%, transparent) !important; - backdrop-filter: var(--backdrop-blur-lg); - -webkit-backdrop-filter: var(--backdrop-blur-lg); -} - -/* Remove backdrop-filter when quick chat is active, because it creates a new - containing block that shifts position:fixed suggest widgets to the right. */ -.monaco-workbench .quick-input-widget:has(.interactive-session) { - backdrop-filter: none; - -webkit-backdrop-filter: none; - background-color: var(--vscode-quickInput-background) !important; } .monaco-workbench.vs-dark .quick-input-widget { @@ -161,10 +142,6 @@ background: transparent !important; } -.monaco-workbench.vs .quick-input-widget .quick-input-list .monaco-list-row:hover:not(.selected):not(.focused) { - background-color: color-mix(in srgb, var(--vscode-list-hoverBackground) 95%, black) !important; -} - .monaco-workbench .quick-input-list .quick-input-list-entry .quick-input-list-separator { height: 16px; margin-top: 2px; @@ -188,10 +165,6 @@ padding: 0; } -.monaco-workbench .monaco-editor .suggest-widget .monaco-list { - border-radius: var(--radius-lg); -} - .monaco-workbench .quick-input-widget .monaco-list-rows { background: transparent !important; } @@ -203,21 +176,12 @@ .monaco-workbench .quick-input-widget .quick-input-filter .monaco-inputbox { background: color-mix(in srgb, var(--vscode-input-background) 60%, transparent) !important; - border-radius: 6px; } /* Chat Widget */ -.monaco-workbench .interactive-session .chat-input-container { - border-radius: var(--radius-lg); -} -.monaco-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor, -.monaco-workbench .interactive-session .chat-editing-session .chat-editing-session-container { - border-radius: var(--radius-lg) var(--radius-lg) 0 0; -} - -.monaco-workbench .interactive-input-part:has(.chat-editing-session > .chat-editing-session-container) .chat-input-container { - border-radius: 0 0 var(--radius-lg) var(--radius-lg); +.monaco-workbench.vs .interactive-session .chat-input-container { + box-shadow: inset var(--shadow-sm); } .monaco-workbench .part.panel .interactive-session, @@ -236,64 +200,15 @@ overflow: visible; } -.monaco-workbench .notification-toast { - box-shadow: none !important; -} - -.monaco-workbench .notifications-list-container { - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); - background: color-mix(in srgb, var(--vscode-notifications-background) 60%, transparent) !important; - box-shadow: var(--shadow-lg) !important; -} - -.monaco-workbench .notifications-center .notifications-list-container { - box-shadow: none !important; -} - -.monaco-workbench .notification-list-item, -.monaco-workbench .notifications-center-header { - background: color-mix(in srgb, var(--vscode-notifications-background) 60%, transparent) !important; -} - -.monaco-workbench > .notifications-toasts .notification-toast-container > .notification-toast { - opacity: 1; -} - -.monaco-workbench .notification-toast-container .notification-toast { - background-color: transparent !important; -} - -.monaco-workbench .notifications-center { - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); - background: color-mix(in srgb, var(--vscode-notifications-background) 60%, transparent) !important; - border: 1px solid var(--vscode-editorWidget-border) !important; - box-shadow: var(--shadow-lg) !important; -} - -.monaco-workbench.vs-dark .notifications-center { - background: color-mix(in srgb, var(--vscode-notifications-background) 60%, transparent) !important; -} - -/* .monaco-workbench .notifications-list-container, -.monaco-workbench > .notifications-center > .notifications-center-header, */ .monaco-workbench .notifications-list-container .monaco-list-rows { background: transparent !important; } /* Context Menus */ -.monaco-workbench .monaco-menu .monaco-action-bar.vertical { - border-radius: var(--radius-lg); -} .monaco-workbench .context-view .monaco-menu { box-shadow: var(--shadow-lg); border: none; - border-radius: var(--radius-lg); - background: color-mix(in srgb, var(--vscode-menu-background) 60%, transparent) !important; - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); } .monaco-workbench .monaco-select-box-dropdown-container { @@ -301,17 +216,7 @@ } .monaco-workbench .monaco-menu-container > .monaco-scrollable-element { - border-radius: var(--radius-lg) !important; box-shadow: var(--shadow-lg) !important; - background: color-mix(in srgb, var(--vscode-menu-background) 60%, transparent) !important; - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); -} - -.monaco-workbench .action-widget { - background: color-mix(in srgb, var(--vscode-menu-background) 60%, transparent) !important; - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); } .monaco-workbench .action-widget .action-widget-action-bar { @@ -321,56 +226,30 @@ /* Suggest Widget */ .monaco-workbench .monaco-editor .suggest-widget { box-shadow: var(--shadow-lg); - border-radius: var(--radius-lg); - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); - background: color-mix(in srgb, var(--vscode-editorSuggestWidget-background) 60%, transparent) !important; } .monaco-workbench.vs-dark .monaco-editor .suggest-widget { - background: color-mix(in srgb, var(--vscode-editorSuggestWidget-background) 60%, transparent) !important; border: 1px solid var(--vscode-editorWidget-border); } /* Find Widget */ .monaco-workbench .monaco-editor .find-widget { box-shadow: var(--shadow-lg); - border-radius: var(--radius-lg); - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); - margin-top: 4px !important; } .monaco-workbench .inline-chat-gutter-menu { - border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); } /* Dialog */ .monaco-workbench .monaco-dialog-box { - box-shadow: var(--shadow-xl); - border-radius: var(--radius-lg); - backdrop-filter: var(--backdrop-blur-lg); - -webkit-backdrop-filter: var(--backdrop-blur-lg); - background: color-mix(in srgb, var(--vscode-editor-background) 60%, transparent) !important; -} - -.monaco-workbench.vs-dark .monaco-dialog-box { border: 1px solid var(--vscode-dialog-border); + box-shadow: var(--shadow-xl); } /* Peek View */ .monaco-workbench .monaco-editor .peekview-widget { box-shadow: var(--shadow-hover); - background: color-mix(in srgb, var(--vscode-peekViewEditor-background) 60%, transparent) !important; - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); -} - -.monaco-workbench.vs-dark .monaco-editor .peekview-widget { - background: color-mix(in srgb, var(--vscode-peekViewEditor-background) 60%, transparent) !important; } .monaco-workbench .monaco-editor .peekview-widget .head, @@ -379,38 +258,22 @@ } .monaco-editor .monaco-hover { - background-color: color-mix(in srgb, var(--vscode-editorHoverWidget-background) 60%, transparent) !important; box-shadow: var(--shadow-sm-strong); - border-radius: var(--radius-lg); - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); } .monaco-workbench .monaco-hover.workbench-hover, .monaco-hover.workbench-hover { - background-color: color-mix(in srgb, var(--vscode-editorHoverWidget-background) 60%, transparent) !important; - backdrop-filter: var(--backdrop-blur-lg) !important; - -webkit-backdrop-filter: var(--backdrop-blur-lg) !important; box-shadow: var(--shadow-sm-strong); } .monaco-workbench .defineKeybindingWidget { - box-shadow: var(--shadow-lg) !important; - border-radius: var(--radius-lg); - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); - background-color: color-mix(in srgb, var(--vscode-editorHoverWidget-background) 60%, transparent) !important; -} - -.monaco-workbench.vs-dark .defineKeybindingWidget { border: 1px solid var(--vscode-editorWidget-border); + box-shadow: var(--shadow-lg) !important; } .monaco-workbench .chat-editor-overlay-widget, .monaco-workbench .chat-diff-change-content-widget { box-shadow: var(--shadow-md); - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); } .monaco-workbench.vs-dark .chat-editor-overlay-widget, @@ -452,15 +315,6 @@ } /* Breadcrumbs */ -.monaco-workbench .breadcrumbs-picker-widget { - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); - background: color-mix(in srgb, var(--vscode-breadcrumbPicker-background) 60%, transparent) !important; -} - -.monaco-workbench.vs-dark .breadcrumbs-picker-widget { - background: color-mix(in srgb, var(--vscode-breadcrumbPicker-background) 60%, transparent) !important; -} .monaco-workbench.vs .breadcrumbs-control { border-bottom: 1px solid var(--vscode-editorWidget-border); @@ -507,28 +361,20 @@ /* Dropdowns */ .monaco-workbench .monaco-dropdown .dropdown-menu { box-shadow: var(--shadow-lg); - border-radius: var(--radius-lg); } /* SCM */ .monaco-workbench .scm-view .scm-provider { box-shadow: var(--shadow-sm); - border-radius: var(--radius-md); } /* Debug Toolbar */ .monaco-workbench .debug-toolbar { box-shadow: var(--shadow-lg); - border-radius: var(--radius-lg); - backdrop-filter: var(--backdrop-blur-lg) !important; - -webkit-backdrop-filter: var(--backdrop-blur-lg) !important; } .monaco-workbench .debug-hover-widget { box-shadow: var(--shadow-lg); - border-radius: var(--radius-lg); - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); color: var(--vscode-editor-foreground) !important; } @@ -539,20 +385,11 @@ /* Action Widget */ .monaco-workbench .action-widget { box-shadow: var(--shadow-lg) !important; - border-radius: var(--radius-lg); } /* Parameter Hints */ .monaco-workbench .monaco-editor .parameter-hints-widget { box-shadow: var(--shadow-lg); - border-radius: var(--radius-lg); - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); -} - -.monaco-workbench.vs-dark .monaco-editor .parameter-hints-widget, -.monaco-workbench.vs .monaco-editor .parameter-hints-widget { - background: color-mix(in srgb, var(--vscode-editorWidget-background) 60%, transparent) !important; } /* Minimap */ @@ -610,8 +447,6 @@ } .monaco-editor .rename-box.preview { - backdrop-filter: var(--backdrop-blur-lg) !important; - -webkit-backdrop-filter: var(--backdrop-blur-lg) !important; box-shadow: var(--shadow-hover) !important; border: 1px solid var(--vscode-editorWidget-border); } @@ -619,38 +454,23 @@ /* Notebook */ .monaco-workbench .notebookOverlay .monaco-list-row .cell-editor-part:before { - border-radius: var(--radius-md); + box-shadow: inset var(--shadow-sm); } .notebookOverlay .monaco-list-row .cell-title-toolbar { background-color: var(--vscode-editorWidget-background) !important; - backdrop-filter: var(--backdrop-blur-md); - -webkit-backdrop-filter: var(--backdrop-blur-md); - border-radius: var(--radius-md); box-shadow: var(--shadow-sm); } -.notebookOverlay .cell-bottom-toolbar-container .action-item { - border-radius: var(--radius-sm); -} - /* Inline Chat */ .monaco-workbench .monaco-editor .inline-chat { box-shadow: var(--shadow-lg); border: none; - border-radius: var(--radius-lg); } /* Command Center */ .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center { - border-radius: var(--radius-lg) !important; - background: color-mix(in srgb, var(--vscode-commandCenter-background) 60%, transparent) !important; - -webkit-backdrop-filter: var(--backdrop-blur-md); - backdrop-filter: var(--backdrop-blur-md); -} - -.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:hover { - background: color-mix(in srgb, var(--vscode-commandCenter-activeBackground) 60%, transparent) !important; + box-shadow: inset var(--shadow-sm) !important; } .monaco-workbench .part.titlebar .command-center .agent-status-pill { @@ -671,10 +491,6 @@ background-color: var(--vscode-commandCenter-activeBackground); } -.monaco-dialog-modal-block .dialog-shadow { - border-radius: var(--radius-lg); -} - .monaco-workbench .unified-quick-access-tabs { background: transparent; } @@ -684,181 +500,3 @@ opacity: 1; color: var(--vscode-descriptionForeground); } - -/* ============================================================================================ - * Reduced Transparency - disable backdrop-filter blur and color-mix transparency effects - * for improved rendering performance. Controlled by workbench.reduceTransparency setting. - * ============================================================================================ */ - -/* Reset blur variables to none */ -.monaco-workbench.monaco-reduce-transparency { - --backdrop-blur-sm: none; - --backdrop-blur-md: none; - --backdrop-blur-lg: none; -} - -/* Quick Input (Command Palette) */ -.monaco-workbench.monaco-reduce-transparency .quick-input-widget { - background-color: var(--vscode-quickInput-background) !important; - -webkit-backdrop-filter: none; - backdrop-filter: none; -} - -/* Notifications */ -.monaco-workbench.monaco-reduce-transparency .notification-toast-container { - -webkit-backdrop-filter: none; - backdrop-filter: none; - background: var(--vscode-notifications-background) !important; -} - -.monaco-workbench.monaco-reduce-transparency .notifications-list-container { - -webkit-backdrop-filter: none; - backdrop-filter: none; - background: var(--vscode-notifications-background) !important; -} - -.monaco-workbench.monaco-reduce-transparency .notification-list-item, -.monaco-workbench.monaco-reduce-transparency .notifications-center-header { - background: var(--vscode-notifications-background) !important; -} - -.monaco-workbench.monaco-reduce-transparency .notifications-center { - -webkit-backdrop-filter: none; - backdrop-filter: none; - background: var(--vscode-notifications-background) !important; -} - -/* Context Menu / Action Widget */ -.monaco-workbench.monaco-reduce-transparency .action-widget { - background: var(--vscode-menu-background) !important; - -webkit-backdrop-filter: none; - backdrop-filter: none; -} - -/* Suggest Widget */ -.monaco-workbench.monaco-reduce-transparency .monaco-editor .suggest-widget { - -webkit-backdrop-filter: none; - backdrop-filter: none; - background: var(--vscode-editorSuggestWidget-background) !important; -} - -/* Find Widget */ -.monaco-workbench.monaco-reduce-transparency .monaco-editor .find-widget { - -webkit-backdrop-filter: none; - backdrop-filter: none; -} - -.monaco-workbench.monaco-reduce-transparency .inline-chat-gutter-menu { - -webkit-backdrop-filter: none; - backdrop-filter: none; -} - -/* Dialog */ -.monaco-workbench.monaco-reduce-transparency .monaco-dialog-box { - -webkit-backdrop-filter: none; - backdrop-filter: none; - background: var(--vscode-editor-background) !important; -} - -/* Peek View */ -.monaco-workbench.monaco-reduce-transparency .monaco-editor .peekview-widget { - -webkit-backdrop-filter: none; - backdrop-filter: none; - background: var(--vscode-peekViewEditor-background) !important; -} - -/* Hover */ -.monaco-reduce-transparency .monaco-hover { - background-color: var(--vscode-editorHoverWidget-background) !important; - -webkit-backdrop-filter: none; - backdrop-filter: none; -} - -.monaco-reduce-transparency .monaco-hover.workbench-hover, -.monaco-reduce-transparency .workbench-hover { - background-color: var(--vscode-editorHoverWidget-background) !important; - -webkit-backdrop-filter: none !important; - backdrop-filter: none !important; -} - -/* Keybinding Widget */ -.monaco-workbench.monaco-reduce-transparency .defineKeybindingWidget { - -webkit-backdrop-filter: none; - backdrop-filter: none; - background: var(--vscode-editorHoverWidget-background) !important; -} - -/* Chat Editor Overlay */ -.monaco-workbench.monaco-reduce-transparency .chat-editor-overlay-widget, -.monaco-workbench.monaco-reduce-transparency .chat-diff-change-content-widget { - -webkit-backdrop-filter: none; - backdrop-filter: none; -} - -/* Debug Toolbar */ -.monaco-workbench.monaco-reduce-transparency .debug-toolbar { - -webkit-backdrop-filter: none !important; - backdrop-filter: none !important; -} - -.monaco-workbench.monaco-reduce-transparency .debug-hover-widget { - -webkit-backdrop-filter: none; - backdrop-filter: none; -} - -/* Parameter Hints */ -.monaco-workbench.monaco-reduce-transparency .monaco-editor .parameter-hints-widget { - -webkit-backdrop-filter: none; - backdrop-filter: none; - background: var(--vscode-editorWidget-background) !important; -} - -/* Sticky Scroll */ -.monaco-workbench.monaco-reduce-transparency .monaco-editor .sticky-widget { - -webkit-backdrop-filter: none !important; - backdrop-filter: none !important; - background: var(--vscode-editor-background) !important; -} - -.monaco-workbench.monaco-reduce-transparency .monaco-editor .sticky-widget .sticky-widget-line-numbers, -.monaco-workbench.monaco-reduce-transparency .monaco-editor .sticky-widget .sticky-widget-lines, -.monaco-workbench.monaco-reduce-transparency .monaco-editor .sticky-widget .sticky-line-content { - -webkit-backdrop-filter: none !important; - backdrop-filter: none !important; - background: var(--vscode-editor-background) !important; -} - -/* Rename Box */ -.monaco-reduce-transparency .monaco-editor .rename-box.preview { - -webkit-backdrop-filter: none !important; - backdrop-filter: none !important; -} - -/* Notebook */ -.monaco-workbench.monaco-reduce-transparency .notebookOverlay .monaco-list-row .cell-title-toolbar { - -webkit-backdrop-filter: none; - backdrop-filter: none; -} - -/* Command Center */ -.monaco-workbench.monaco-reduce-transparency .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center { - background: var(--vscode-commandCenter-background) !important; - -webkit-backdrop-filter: none; - backdrop-filter: none; -} - -.monaco-workbench.monaco-reduce-transparency .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:hover { - background: var(--vscode-commandCenter-activeBackground) !important; -} - -/* Breadcrumbs */ -.monaco-workbench.monaco-reduce-transparency .breadcrumbs-picker-widget { - -webkit-backdrop-filter: none; - backdrop-filter: none; - background: var(--vscode-breadcrumbPicker-background) !important; -} - -/* Quick Input filter input */ -.monaco-workbench.monaco-reduce-transparency .quick-input-widget .quick-input-filter .monaco-inputbox { - background: var(--vscode-input-background) !important; -} diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index c484fa86dbd94..844b50bec385d 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -30,7 +30,7 @@ min-height: 75px; padding: 10px; transform: translate3d(0px, 0px, 0px); - border-radius: 3px; + border-radius: var(--vscode-cornerRadius-large); } .monaco-dialog-box.align-vertical { @@ -238,3 +238,7 @@ .monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button-dropdown > .monaco-dropdown-button { padding: 0 4px; } + +.monaco-dialog-modal-block .dialog-shadow { + border-radius: var(--vscode-cornerRadius-large); +} diff --git a/src/vs/base/browser/ui/dropdown/dropdown.css b/src/vs/base/browser/ui/dropdown/dropdown.css index bfcaee41f98ad..0e2d6bdd9f53c 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.css +++ b/src/vs/base/browser/ui/dropdown/dropdown.css @@ -20,6 +20,10 @@ cursor: default; } +.monaco-dropdown .dropdown-menu { + border-radius: var(--vscode-cornerRadius-large); +} + .monaco-dropdown-with-primary { display: flex !important; flex-direction: row; diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 8e29f4924766b..5f50659ff46c2 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ChildProcess, spawn, SpawnOptions, StdioOptions } from 'child_process'; -import { chmodSync, existsSync, readFileSync, statSync, truncateSync, unlinkSync } from 'fs'; +import { chmodSync, existsSync, readFileSync, statSync, truncateSync, unlinkSync, promises } from 'fs'; import { homedir, tmpdir } from 'os'; import type { ProfilingSession, Target } from 'v8-inspect-profiler'; import { Event } from '../../base/common/event.js'; @@ -499,8 +499,26 @@ export async function main(argv: string[]): Promise { // This way, Mac does not automatically try to foreground the new instance, which causes // focusing issues when the new instance only sends data to a previous instance and then closes. const spawnArgs = ['-n', '-g']; - // -a opens the given application. - spawnArgs.push('-a', process.execPath); // -a: opens a specific application + + // Figure out the app to launch: with --sessions we try to launch the embedded app + let appToLaunch = process.execPath; + if (args.sessions) { + // process.execPath is e.g. /Applications/Code.app/Contents/MacOS/Electron + // Embedded app is at /Applications/Code.app/Contents/Applications/.app + const contentsPath = dirname(dirname(process.execPath)); + const applicationsPath = join(contentsPath, 'Applications'); + try { + const files = await promises.readdir(applicationsPath); + const embeddedApp = files.find(file => file.endsWith('.app')); + if (embeddedApp) { + appToLaunch = join(applicationsPath, embeddedApp); + argv = argv.filter(arg => arg !== '--sessions'); + } + } catch (error) { + /* may not exist on disk */ + } + } + spawnArgs.push('-a', appToLaunch); // -a opens the given application. if (args.verbose || args.status) { spawnArgs.push('--wait-apps'); // `open --wait-apps`: blocks until the launched app is closed (even if they were already running) diff --git a/src/vs/editor/contrib/find/browser/findWidget.css b/src/vs/editor/contrib/find/browser/findWidget.css index f1dd6191b9580..cbe6028775c93 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.css +++ b/src/vs/editor/contrib/find/browser/findWidget.css @@ -12,15 +12,13 @@ line-height: 19px; transition: transform 200ms linear; padding: 0 4px 0 9px; + margin-top: 4px; box-sizing: border-box; transform: translateY(calc(-100% - 10px)); /* shadow (10px) */ box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); color: var(--vscode-editorWidget-foreground); - border-left: 1px solid var(--vscode-widget-border); - border-right: 1px solid var(--vscode-widget-border); - border-bottom: 1px solid var(--vscode-widget-border); - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; + border: 1px solid var(--vscode-widget-border); + border-radius: var(--vscode-cornerRadius-large); background-color: var(--vscode-editorWidget-background); } diff --git a/src/vs/editor/contrib/hover/browser/hover.css b/src/vs/editor/contrib/hover/browser/hover.css index aedcb6944b324..b33ea5e76bce9 100644 --- a/src/vs/editor/contrib/hover/browser/hover.css +++ b/src/vs/editor/contrib/hover/browser/hover.css @@ -9,7 +9,7 @@ .monaco-editor .monaco-resizable-hover { border: 1px solid var(--vscode-editorHoverWidget-border); - border-radius: 3px; + border-radius: var(--vscode-cornerRadius-large); box-sizing: content-box; } @@ -20,7 +20,7 @@ .monaco-editor .monaco-hover { border: 1px solid var(--vscode-editorHoverWidget-border); - border-radius: 3px; + border-radius: var(--vscode-cornerRadius-large); color: var(--vscode-editorHoverWidget-foreground); background-color: var(--vscode-editorHoverWidget-background); } diff --git a/src/vs/editor/contrib/parameterHints/browser/parameterHints.css b/src/vs/editor/contrib/parameterHints/browser/parameterHints.css index 3efac6c122caa..bf54d22d60ed9 100644 --- a/src/vs/editor/contrib/parameterHints/browser/parameterHints.css +++ b/src/vs/editor/contrib/parameterHints/browser/parameterHints.css @@ -13,6 +13,7 @@ color: var(--vscode-editorHoverWidget-foreground); background-color: var(--vscode-editorHoverWidget-background); border: 1px solid var(--vscode-editorHoverWidget-border); + border-radius: var(--vscode-cornerRadius-large); } .hc-black .monaco-editor .parameter-hints-widget, diff --git a/src/vs/editor/contrib/suggest/browser/media/suggest.css b/src/vs/editor/contrib/suggest/browser/media/suggest.css index 755c457fc20bd..2d9fd8b1f7c01 100644 --- a/src/vs/editor/contrib/suggest/browser/media/suggest.css +++ b/src/vs/editor/contrib/suggest/browser/media/suggest.css @@ -10,7 +10,7 @@ z-index: 40; display: flex; flex-direction: column; - border-radius: 3px; + border-radius: var(--vscode-cornerRadius-large); } .monaco-editor .suggest-widget.message { @@ -96,6 +96,7 @@ .monaco-editor .suggest-widget .monaco-list { user-select: none; -webkit-user-select: none; + border-radius: var(--vscode-cornerRadius-large); } /** Styles for each row in the list element **/ diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index bd509719a3cce..bfad8086272b0 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -9,7 +9,7 @@ z-index: 2550; left: 50%; -webkit-app-region: no-drag; - border-radius: 8px; + border-radius: var(--vscode-cornerRadius-xLarge); } .quick-input-titlebar { @@ -97,6 +97,10 @@ padding: 6px 6px 4px 6px; } +.quick-input-widget .quick-input-filter .monaco-inputbox { + border-radius: var(--vscode-cornerRadius-medium); +} + .quick-input-widget.hidden-input .quick-input-header { /* reduce margins and paddings when input box hidden */ padding: 0; diff --git a/src/vs/sessions/LAYOUT.md b/src/vs/sessions/LAYOUT.md index fea9a1c370b86..ad54a086c7fb1 100644 --- a/src/vs/sessions/LAYOUT.md +++ b/src/vs/sessions/LAYOUT.md @@ -440,7 +440,7 @@ The `AuxiliaryBarPart` provides a custom `DropdownWithPrimaryActionViewItem` for The `SidebarPart` includes a footer section (35px height) positioned below the pane composite content. The sidebar uses a custom `layout()` override that reduces the content height by `FOOTER_HEIGHT` and renders a `MenuWorkbenchToolBar` driven by `Menus.SidebarFooter`. The footer hosts the account widget (see Section 3.6). -On macOS native, the sidebar title area includes a traffic light spacer (70px) to push content past the system window controls, which is hidden in fullscreen mode. +On macOS native with custom titlebar, the sidebar title area includes a traffic light spacer (70px) to push content past the system window controls. The spacer is hidden in fullscreen mode and is not created when using native titlebar (since the OS renders traffic lights in its own title bar). --- @@ -640,6 +640,7 @@ interface IPartVisibilityState { | Date | Change | |------|--------| +| 2026-03-02 | Fixed macOS sidebar traffic light spacer to only render with custom titlebar; added `!hasNativeTitlebar()` guard to `SidebarPart.createTitleArea()` so the 70px spacer is not created when using native titlebar (traffic lights are in the OS title bar, not overlapping the sidebar) | | 2026-02-20 | Replaced custom `EditorModal` with standard `ModalEditorPart` via `MODAL_GROUP`; main editor part created but hidden; changed `workbench.editor.useModal` from boolean to enum (`off`/`some`/`all`); sessions config uses `all`; removed `editorModal.ts` and editor modal CSS | | 2026-02-17 | Added `-webkit-app-region: drag` to sidebar title area so it can be used to drag the window; interactive children (actions, composite bar, labels) marked `no-drag`; CSS rules scoped to `.agent-sessions-workbench` in `parts/media/sidebarPart.css` | | 2026-02-13 | Documentation sync: Updated all file names, class names, and references to match current implementation. `AgenticWorkbench` → `Workbench`, `AgenticSidebarPart` → `SidebarPart`, `AgenticAuxiliaryBarPart` → `AuxiliaryBarPart`, `AgenticPanelPart` → `PanelPart`, `agenticWorkbench.ts` → `workbench.ts`, `agenticWorkbenchMenus.ts` → `menus.ts`, `agenticLayoutActions.ts` → `layoutActions.ts`, `AgenticTitleBarWidget` → `SessionsTitleBarWidget`, `AgenticTitleBarContribution` → `SessionsTitleBarContribution`. Removed references to deleted files (`sidebarRevealButton.ts`, `floatingToolbar.ts`, `agentic.contributions.ts`, `agenticTitleBarWidget.ts`). Updated pane composite architecture from `SyncDescriptor`-based to `AgenticPaneCompositePartService`. Moved account widget docs from titlebar to sidebar footer. Added documentation for sidebar footer, project bar, traffic light spacer, card appearance styling, widget directory, and new contrib structure (`accountMenu/`, `chat/`, `configuration/`, `sessions/`). Updated titlebar actions to reflect Run Script split button and Open submenu. Removed Toggle Maximize panel action (no longer registered). Updated contributions section with all current contributions and their locations. | diff --git a/src/vs/sessions/browser/media/style.css b/src/vs/sessions/browser/media/style.css index c79f7e8d21179..6947f2aff496c 100644 --- a/src/vs/sessions/browser/media/style.css +++ b/src/vs/sessions/browser/media/style.css @@ -46,7 +46,7 @@ /* ---- Chat Input ---- */ .agent-sessions-workbench .interactive-session .chat-input-container { - border-radius: 8px !important; + border-radius: var(--vscode-cornerRadius-large) !important; } .agent-sessions-workbench .interactive-session .interactive-input-part { diff --git a/src/vs/sessions/browser/parts/sidebarPart.ts b/src/vs/sessions/browser/parts/sidebarPart.ts index 5f8cce31cf807..b9132d4d13e63 100644 --- a/src/vs/sessions/browser/parts/sidebarPart.ts +++ b/src/vs/sessions/browser/parts/sidebarPart.ts @@ -38,6 +38,8 @@ import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../platform/acti import { isMacintosh, isNative } from '../../../base/common/platform.js'; import { isFullscreen, onDidChangeFullscreen } from '../../../base/browser/browser.js'; import { mainWindow } from '../../../base/browser/window.js'; +import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; +import { hasNativeTitlebar, getTitleBarStyle } from '../../../platform/window/common/window.js'; /** * Sidebar part specifically for agent sessions workbench. @@ -103,6 +105,7 @@ export class SidebarPart extends AbstractPaneCompositePart { @IContextKeyService contextKeyService: IContextKeyService, @IExtensionService extensionService: IExtensionService, @IMenuService menuService: IMenuService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super( Parts.SIDEBAR_PART, @@ -151,7 +154,7 @@ export class SidebarPart extends AbstractPaneCompositePart { // macOS native: the sidebar spans full height and the traffic lights // overlay the top-left corner. Add a fixed-width spacer inside the // title area to push content horizontally past the traffic lights. - if (titleArea && isMacintosh && isNative) { + if (titleArea && isMacintosh && isNative && !hasNativeTitlebar(this.configurationService, getTitleBarStyle(this.configurationService))) { const spacer = $('div.window-controls-container'); spacer.style.width = '70px'; spacer.style.height = '100%'; diff --git a/src/vs/sessions/contrib/applyToParentRepo/browser/applyToParentRepo.contribution.ts b/src/vs/sessions/contrib/applyToParentRepo/browser/applyToParentRepo.contribution.ts index 96a23763e803c..3b5ce530a7b12 100644 --- a/src/vs/sessions/contrib/applyToParentRepo/browser/applyToParentRepo.contribution.ts +++ b/src/vs/sessions/contrib/applyToParentRepo/browser/applyToParentRepo.contribution.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { toAction } from '../../../../base/common/actions.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../base/common/network.js'; import { autorun } from '../../../../base/common/observable.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { localize, localize2 } from '../../../../nls.js'; @@ -11,7 +13,9 @@ import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/c import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IFileService } from '../../../../platform/files/common/files.js'; -import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; @@ -83,6 +87,8 @@ class ApplyToParentRepoAction extends Action2 { const fileService = accessor.get(IFileService); const notificationService = accessor.get(INotificationService); const logService = accessor.get(ILogService); + const openerService = accessor.get(IOpenerService); + const productService = accessor.get(IProductService); const activeSession = sessionManagementService.getActiveSession(); if (!activeSession?.worktree || !activeSession?.repository) { @@ -139,19 +145,46 @@ class ApplyToParentRepoAction extends Action2 { } } + const openFolderAction = toAction({ + id: 'applyToParentRepo.openFolder', + label: localize('openInVSCode', "Open in VS Code"), + run: () => { + const scheme = productService.quality === 'stable' + ? 'vscode' + : productService.quality === 'exploration' + ? 'vscode-exploration' + : 'vscode-insiders'; + + const params = new URLSearchParams(); + params.set('windowId', '_blank'); + params.set('session', activeSession.resource.toString()); + + openerService.open(URI.from({ + scheme, + authority: Schemas.file, + path: repoRoot.path, + query: params.toString(), + }), { openExternal: true }); + } + }); + const totalApplied = copiedCount + deletedCount; if (errorCount > 0) { - notificationService.warn( - totalApplied === 1 + notificationService.notify({ + severity: Severity.Warning, + message: totalApplied === 1 ? localize('applyToParentRepoPartial1', "Applied 1 file to parent repo with {0} error(s).", errorCount) - : localize('applyToParentRepoPartialN', "Applied {0} files to parent repo with {1} error(s).", totalApplied, errorCount) - ); + : localize('applyToParentRepoPartialN', "Applied {0} files to parent repo with {1} error(s).", totalApplied, errorCount), + actions: { primary: [openFolderAction] } + }); } else if (totalApplied > 0) { - notificationService.info( - totalApplied === 1 + notificationService.notify({ + severity: Severity.Info, + message: totalApplied === 1 ? localize('applyToParentRepoSuccess1', "Applied 1 file to parent repo.") - : localize('applyToParentRepoSuccessN', "Applied {0} files to parent repo.", totalApplied) - ); + : localize('applyToParentRepoSuccessN', "Applied {0} files to parent repo.", totalApplied), + actions: { primary: [openFolderAction] } + }); } } } diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 2d29fbe475e0c..47eca5b0933e9 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -661,11 +661,9 @@ export class ChangesViewPane extends ViewPane { }, compressionEnabled: true, twistieAdditionalCssClass: (e: unknown) => { - if (this.viewMode === ChangesViewMode.List) { - return 'force-no-twistie'; - } - // In tree mode, hide twistie for file items (they are never collapsible) - return isChangesFileItem(e as ChangesTreeElement) ? 'force-no-twistie' : undefined; + return this.viewMode === ChangesViewMode.List + ? 'force-no-twistie' + : undefined; }, } ); @@ -675,6 +673,9 @@ export class ChangesViewPane extends ViewPane { if (this.tree) { const tree = this.tree; + // Re-layout when collapse state changes so the card height adjusts + this.renderDisposables.add(tree.onDidChangeContentHeight(() => this.layoutTree())); + const openFileItem = (item: IChangesFileItem, items: IChangesFileItem[], sideBySide: boolean) => { const { uri: modifiedFileUri, originalUri, isDeletion } = item; const currentIndex = items.indexOf(item); diff --git a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts index 0ace6677cb836..74b536c694779 100644 --- a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts @@ -17,7 +17,7 @@ import { IViewContainersRegistry, IViewsRegistry, ViewContainerLocation, Extensi import { Registry } from '../../../../platform/registry/common/platform.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; -import { ISessionsManagementService, IsNewChatSessionContext } from '../../sessions/browser/sessionsManagementService.js'; +import { IsActiveSessionBackgroundProviderContext, ISessionsManagementService, IsNewChatSessionContext } from '../../sessions/browser/sessionsManagementService.js'; import { Menus } from '../../../browser/menus.js'; import { BranchChatSessionAction } from './branchChatSessionAction.js'; import { RunScriptContribution } from './runScriptAction.js'; @@ -51,7 +51,7 @@ export class OpenSessionWorktreeInVSCodeAction extends Action2 { id: Menus.TitleBarRight, group: 'navigation', order: 10, - when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()) + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext) }] }); } @@ -92,6 +92,29 @@ export class OpenSessionWorktreeInVSCodeAction extends Action2 { } registerAction2(OpenSessionWorktreeInVSCodeAction); +// Disabled placeholder shown in the titlebar when the active session does not support opening in VS Code +class OpenSessionWorktreeInVSCodeNotAvailableAction extends Action2 { + constructor() { + super({ + id: 'chat.openSessionWorktreeInVSCode.notAvailable', + title: localize2('openInVSCode', 'Open in VS Code'), + tooltip: localize('openInVSCodeNotAvailableTooltip', "Open in VS Code is not available for this session type"), + icon: Codicon.vscodeInsiders, + precondition: ContextKeyExpr.false(), + menu: [{ + id: Menus.TitleBarRight, + group: 'navigation', + order: 10, + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext.toNegated()) + }] + }); + } + + override run(): void { } +} + +registerAction2(OpenSessionWorktreeInVSCodeNotAvailableAction); + class NewChatInSessionsWindowAction extends Action2 { constructor() { diff --git a/src/vs/sessions/contrib/chat/browser/folderPicker.ts b/src/vs/sessions/contrib/chat/browser/folderPicker.ts index 22e53aa45654f..dbc253e1cbfa2 100644 --- a/src/vs/sessions/contrib/chat/browser/folderPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/folderPicker.ts @@ -7,7 +7,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { basename, isEqual } from '../../../../base/common/resources.js'; +import { basename, extUriBiasedIgnorePathCase, isEqual } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js'; @@ -27,6 +27,7 @@ const FILTER_THRESHOLD = 10; interface IFolderItem { readonly uri: URI; readonly label: string; + readonly checked?: boolean; } /** @@ -218,40 +219,32 @@ export class FolderPicker extends Disposable { private _buildItems(currentFolderUri: URI | undefined): IActionListItem[] { const seenUris = new Set(); - if (currentFolderUri) { - seenUris.add(currentFolderUri.toString()); - } const items: IActionListItem[] = []; - // Currently selected folder (shown first, checked) + // Collect all folders (current + recently picked), deduplicated and sorted by name + const allFolders: { uri: URI; label: string }[] = []; if (currentFolderUri) { - items.push({ - kind: ActionListItemKind.Action, - label: basename(currentFolderUri), - group: { title: '', icon: Codicon.folder }, - item: { uri: currentFolderUri, label: basename(currentFolderUri) }, - }); + seenUris.add(currentFolderUri.toString()); + allFolders.push({ uri: currentFolderUri, label: basename(currentFolderUri) }); } - - // Recently picked folders (sorted by name) - const dedupedFolders: { uri: URI; label: string }[] = []; for (const folderUri of this._recentlyPickedFolders) { const key = folderUri.toString(); if (seenUris.has(key)) { continue; } seenUris.add(key); - dedupedFolders.push({ uri: folderUri, label: basename(folderUri) }); + allFolders.push({ uri: folderUri, label: basename(folderUri) }); } - dedupedFolders.sort((a, b) => a.label.localeCompare(b.label)); - for (const folder of dedupedFolders) { + allFolders.sort((a, b) => extUriBiasedIgnorePathCase.compare(a.uri, b.uri)); + for (const folder of allFolders) { + const isCurrent = currentFolderUri && isEqual(folder.uri, currentFolderUri); items.push({ kind: ActionListItemKind.Action, label: folder.label, group: { title: '', icon: Codicon.folder }, - item: { uri: folder.uri, label: folder.label }, - onRemove: () => this._removeFolder(folder.uri), + item: { uri: folder.uri, label: folder.label, checked: isCurrent || false }, + ...(!isCurrent ? { onRemove: () => this._removeFolder(folder.uri) } : {}), }); } diff --git a/src/vs/sessions/contrib/chat/browser/repoPicker.ts b/src/vs/sessions/contrib/chat/browser/repoPicker.ts index bc263c8234a1d..95e8b039df569 100644 --- a/src/vs/sessions/contrib/chat/browser/repoPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/repoPicker.ts @@ -15,7 +15,6 @@ import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js' import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { INewSession } from './newSession.js'; import { URI } from '../../../../base/common/uri.js'; -import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; const OPEN_REPO_COMMAND = 'github.copilot.chat.cloudSessions.openRepository'; const STORAGE_KEY_LAST_REPO = 'agentSessions.lastPickedRepo'; @@ -271,7 +270,7 @@ export class RepoPicker extends Disposable { } private _setRepo(repo: IRepoItem): void { - this._newSession?.setRepoUri(URI.parse(`${GITHUB_REMOTE_FILE_SCHEME}://github/${repo.id}`)); + this._newSession?.setRepoUri(URI.parse(`vscode-vfs://github/${repo.id}`)); } } diff --git a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts index bacdbac4330b8..e5c024cb2ba0d 100644 --- a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts +++ b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts @@ -9,13 +9,13 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { autorun, derivedOpts, IObservable } from '../../../../base/common/observable.js'; import { localize, localize2 } from '../../../../nls.js'; import { MenuId, registerAction2, Action2, MenuRegistry } from '../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; import { SessionsCategories } from '../../../common/categories.js'; -import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { IActiveSessionItem, IsActiveSessionBackgroundProviderContext, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { Menus } from '../../../browser/menus.js'; import { ISessionsConfigurationService, ITaskEntry, TaskStorageTarget } from './sessionsConfigurationService.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js'; import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; @@ -284,7 +284,7 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr } } -// Register the Run split button submenu on the workbench title bar +// Register the Run split button submenu on the workbench title bar (background sessions only) MenuRegistry.appendMenuItem(Menus.TitleBarRight, { submenu: RunScriptDropdownMenuId, isSplitButton: true, @@ -292,5 +292,28 @@ MenuRegistry.appendMenuItem(Menus.TitleBarRight, { icon: Codicon.play, group: 'navigation', order: 8, - when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()) + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext) }); + +// Disabled placeholder shown in the titlebar when the active session does not support running scripts +class RunScriptNotAvailableAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.agentSessions.runScript.notAvailable', + title: localize2('run', "Run"), + tooltip: localize('runScriptNotAvailableTooltip', "Run Script is not available for this session type"), + icon: Codicon.play, + precondition: ContextKeyExpr.false(), + menu: [{ + id: Menus.TitleBarRight, + group: 'navigation', + order: 8, + when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated(), IsActiveSessionBackgroundProviderContext.toNegated()) + }] + }); + } + + override run(): void { } +} + +registerAction2(RunScriptNotAvailableAction); diff --git a/src/vs/sessions/contrib/chat/browser/syncIndicator.ts b/src/vs/sessions/contrib/chat/browser/syncIndicator.ts index e07089daee537..b63411982720a 100644 --- a/src/vs/sessions/contrib/chat/browser/syncIndicator.ts +++ b/src/vs/sessions/contrib/chat/browser/syncIndicator.ts @@ -7,6 +7,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { autorun } from '../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; @@ -24,6 +25,7 @@ export class SyncIndicator extends Disposable { private _repository: IGitRepository | undefined; private _selectedBranch: string | undefined; private _visible = true; + private _syncing = false; private readonly _renderDisposables = this._register(new DisposableStore()); private readonly _stateDisposables = this._register(new DisposableStore()); @@ -81,13 +83,13 @@ export class SyncIndicator extends Disposable { this._renderDisposables.add(dom.addDisposableListener(button, dom.EventType.CLICK, (e) => { dom.EventHelper.stop(e, true); - this.commandService.executeCommand(GIT_SYNC_COMMAND, this._repository?.rootUri); + this._executeSyncCommand(); })); this._renderDisposables.add(dom.addDisposableListener(button, dom.EventType.KEY_DOWN, (e) => { if (e.key === 'Enter' || e.key === ' ') { dom.EventHelper.stop(e, true); - this.commandService.executeCommand(GIT_SYNC_COMMAND, this._repository?.rootUri); + this._executeSyncCommand(); } })); @@ -102,6 +104,20 @@ export class SyncIndicator extends Disposable { this._update(); } + private async _executeSyncCommand(): Promise { + if (this._syncing) { + return; + } + this._syncing = true; + this._update(); + try { + await this.commandService.executeCommand(GIT_SYNC_COMMAND, this._repository?.rootUri); + } finally { + this._syncing = false; + this._update(); + } + } + private _getAheadBehind(): { ahead: number; behind: number } | undefined { if (!this._repository) { return undefined; @@ -132,7 +148,7 @@ export class SyncIndicator extends Disposable { } const counts = this._getAheadBehind(); - if (!counts || !this._visible) { + if ((!counts && !this._syncing) || !this._visible) { this._slotElement.style.display = 'none'; return; } @@ -140,24 +156,26 @@ export class SyncIndicator extends Disposable { this._slotElement.style.display = ''; dom.clearNode(this._buttonElement); - dom.append(this._buttonElement, renderIcon(Codicon.sync)); + dom.append(this._buttonElement, renderIcon(this._syncing ? ThemeIcon.modify(Codicon.sync, 'spin') : Codicon.sync)); - const parts: string[] = []; - if (counts.behind > 0) { - parts.push(`${counts.behind}↓`); - } - if (counts.ahead > 0) { - parts.push(`${counts.ahead}↑`); - } + if (counts) { + const parts: string[] = []; + if (counts.behind > 0) { + parts.push(`${counts.behind}↓`); + } + if (counts.ahead > 0) { + parts.push(`${counts.ahead}↑`); + } - const label = dom.append(this._buttonElement, dom.$('span.sessions-chat-dropdown-label')); - label.textContent = parts.join('\u00a0'); + const label = dom.append(this._buttonElement, dom.$('span.sessions-chat-dropdown-label')); + label.textContent = parts.join('\u00a0'); + } this._buttonElement.title = localize( 'syncIndicator.tooltip', "Synchronize Changes ({0} to pull, {1} to push)", - counts.behind, - counts.ahead, + counts?.behind ?? 0, + counts?.ahead ?? 0, ); } } diff --git a/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts b/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts index 65869c6cb8b5b..2aed42bfd4cf4 100644 --- a/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts +++ b/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun } from '../../../../base/common/observable.js'; +import { autorun, observableValue } from '../../../../base/common/observable.js'; import { Codicon } from '../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; @@ -27,6 +28,7 @@ class GitSyncContribution extends Disposable implements IWorkbenchContribution { private readonly _syncActionDisposable = this._register(new MutableDisposable()); private readonly _gitRepoDisposables = this._register(new DisposableStore()); + private readonly _isSyncing = observableValue(this, false); constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -60,6 +62,7 @@ class GitSyncContribution extends Disposable implements IWorkbenchContribution { } repoDisposables.add(autorun(innerReader => { const state = repository.state.read(innerReader); + const isSyncing = this._isSyncing.read(innerReader); const head = state.HEAD; if (!head?.upstream) { this._syncActionDisposable.clear(); @@ -70,14 +73,16 @@ class GitSyncContribution extends Disposable implements IWorkbenchContribution { const behind = head.behind ?? 0; const hasSyncChanges = ahead > 0 || behind > 0; contextKey.set(hasSyncChanges); - this._syncActionDisposable.value = registerSyncAction(behind, ahead); + this._syncActionDisposable.value = registerSyncAction(behind, ahead, isSyncing, (syncing) => { + this._isSyncing.set(syncing, undefined); + }); })); }); })); } } -function registerSyncAction(behind: number, ahead: number): IDisposable { +function registerSyncAction(behind: number, ahead: number, isSyncing: boolean, setSyncing: (syncing: boolean) => void): IDisposable { if (behind === 0 && ahead === 0) { return Disposable.None; } @@ -89,6 +94,8 @@ function registerSyncAction(behind: number, ahead: number): IDisposable { title += `${ahead}↑`; } + const icon = isSyncing ? ThemeIcon.modify(Codicon.sync, 'spin') : Codicon.sync; + class SynchronizeChangesAction extends Action2 { static readonly ID = 'chatEditing.synchronizeChanges'; @@ -97,7 +104,7 @@ function registerSyncAction(behind: number, ahead: number): IDisposable { id: SynchronizeChangesAction.ID, title, tooltip: localize('synchronizeChanges', "Synchronize Changes with Git (Behind {0}, Ahead {1})", behind, ahead), - icon: Codicon.sync, + icon, category: CHAT_CATEGORY, menu: [ { @@ -114,7 +121,12 @@ function registerSyncAction(behind: number, ahead: number): IDisposable { const commandService = accessor.get(ICommandService); const sessionManagementService = accessor.get(ISessionsManagementService); const worktreeUri = sessionManagementService.getActiveSession()?.worktree; - await commandService.executeCommand('git.sync', worktreeUri); + setSyncing(true); + try { + await commandService.executeCommand('git.sync', worktreeUri); + } finally { + setSyncing(false); + } } } return registerAction2(SynchronizeChangesAction); diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index 85f4a6cbd63d7..d929df11a2d28 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -7,6 +7,7 @@ import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IObservable, observableValue } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; +import { localize } from '../../../../nls.js'; import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -23,10 +24,15 @@ import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browse import { INewSession, LocalNewSession, RemoteNewSession } from '../../chat/browser/newSession.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js'; -import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; export const IsNewChatSessionContext = new RawContextKey('isNewChatSession', true); +/** + * True when the active session uses the Background provider type (copilotcli). + * Used to gate actions that require a local worktree (run script, open in VS Code, terminal). + */ +export const IsActiveSessionBackgroundProviderContext = new RawContextKey('isActiveSessionBackgroundProvider', false, localize('isActiveSessionBackgroundProvider', "Whether the active session uses the background agent provider")); + //#region Active Session Service const LAST_SELECTED_SESSION_KEY = 'agentSessions.lastSelectedSession'; @@ -105,6 +111,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa private readonly _newSession = this._register(new MutableDisposable()); private lastSelectedSession: URI | undefined; private readonly isNewChatSessionContext: IContextKey; + private readonly _isBackgroundProvider: IContextKey; constructor( @IStorageService private readonly storageService: IStorageService, @@ -124,6 +131,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa // Bind context key to active session state. // isNewSession is false when there are any established sessions in the model. this.isNewChatSessionContext = IsNewChatSessionContext.bindTo(contextKeyService); + this._isBackgroundProvider = IsActiveSessionBackgroundProviderContext.bindTo(contextKeyService); // Load last selected session this.lastSelectedSession = this.loadLastSelectedSession(); @@ -194,7 +202,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } if (session.providerType === AgentSessionProviders.Cloud) { - return [URI.parse(`${GITHUB_REMOTE_FILE_SCHEME}://github/${metadata.owner}/${metadata.name}`), undefined]; + return [URI.parse(`vscode-vfs://github/${metadata.owner}/${metadata.name}`), undefined]; } const workingDirectoryPath = metadata?.workingDirectoryPath as string | undefined; @@ -464,6 +472,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa this.logService.trace('[ActiveSessionService] Active session cleared'); } + this._isBackgroundProvider.set(activeSessionItem?.providerType === AgentSessionProviders.Background); this._activeSession.set(activeSessionItem, undefined); } @@ -478,7 +487,8 @@ export class SessionsManagementService extends Disposable implements ISessionsMa a.label === b.label && a.resource.toString() === b.resource.toString() && a.repository?.toString() === b.repository?.toString() && - a.worktree?.toString() === b.worktree?.toString() + a.worktree?.toString() === b.worktree?.toString() && + a.providerType === b.providerType ); } diff --git a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts index 38c8e785ebb74..269d3a63a23bd 100644 --- a/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts +++ b/src/vs/sessions/contrib/terminal/browser/sessionsTerminalContribution.ts @@ -13,6 +13,7 @@ import { Action2, registerAction2 } from '../../../../platform/actions/common/ac import { ILogService } from '../../../../platform/log/common/log.js'; import { IWorkbenchContribution, getWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; +import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { ITerminalService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; import { IPathService } from '../../../../workbench/services/path/common/pathService.js'; import { Menus } from '../../../browser/menus.js'; @@ -22,11 +23,15 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js'; /** - * Returns the cwd URI for the given session: worktree for non-cloud agent - * sessions, repository otherwise, or `undefined` when neither is available. + * Returns the cwd URI for the given session: worktree or repository path for + * background sessions only. Returns `undefined` for non-background sessions + * (Cloud, Local, etc.) which have no local worktree, or when no path is available. */ function getSessionCwd(session: IActiveSessionItem | undefined): URI | undefined { - return session?.worktree ?? session?.repository; + if (session?.providerType !== AgentSessionProviders.Background) { + return undefined; + } + return session.worktree ?? session.repository; } /** @@ -48,14 +53,14 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben @ITerminalService private readonly _terminalService: ITerminalService, @IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService, @ILogService private readonly _logService: ILogService, + @IPathService private readonly _pathService: IPathService, ) { super(); - // React to active session worktree/repository path changes + // React to active session changes — use worktree/repo for background sessions, home dir otherwise this._register(autorun(reader => { const session = this._sessionsManagementService.activeSession.read(reader); - const targetPath = getSessionCwd(session); - this._onActivePathChanged(targetPath); + this._onActiveSessionChanged(session); })); // When a session is archived, close all terminals for its worktree @@ -103,11 +108,14 @@ export class SessionsTerminalContribution extends Disposable implements IWorkben } } - private async _onActivePathChanged(targetPath: URI | undefined): Promise { - if (!targetPath) { + private async _onActiveSessionChanged(session: IActiveSessionItem | undefined): Promise { + if (!session) { return; } + const sessionCwd = getSessionCwd(session); + + const targetPath = sessionCwd ?? await this._pathService.userHome(); const targetFsPath = targetPath.fsPath; if (this._lastTargetFsPath?.toLowerCase() === targetFsPath.toLowerCase()) { return; @@ -143,7 +151,7 @@ class OpenSessionInTerminalAction extends Action2 { menu: [{ id: Menus.TitleBarRight, group: 'navigation', - order: 9, + order: 11, when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()) }] }); diff --git a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts index 5df597398ff60..5cb061bb85e5c 100644 --- a/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts +++ b/src/vs/sessions/contrib/terminal/test/browser/sessionsTerminalContribution.test.ts @@ -18,6 +18,10 @@ import { IAgentSession, IAgentSessionsModel } from '../../../../../workbench/con import { AgentSessionProviders } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { IActiveSessionItem, ISessionsManagementService } from '../../../sessions/browser/sessionsManagementService.js'; import { SessionsTerminalContribution } from '../../browser/sessionsTerminalContribution.js'; +import { TestPathService } from '../../../../../workbench/test/browser/workbenchTestServices.js'; +import { IPathService } from '../../../../../workbench/services/path/common/pathService.js'; + +const HOME_DIR = URI.file('/home/user'); function makeAgentSession(opts: { repository?: URI; @@ -39,10 +43,11 @@ function makeAgentSession(opts: { } as unknown as IActiveSessionItem & IAgentSession; } -function makeNonAgentSession(opts: { repository?: URI; worktree?: URI }): IActiveSessionItem { +function makeNonAgentSession(opts: { repository?: URI; worktree?: URI; providerType?: string }): IActiveSessionItem { return { repository: opts.repository, worktree: opts.worktree, + providerType: opts.providerType ?? AgentSessionProviders.Local, } as IActiveSessionItem; } @@ -111,6 +116,8 @@ suite('SessionsTerminalContribution', () => { } as unknown as IAgentSessionsModel; }); + instantiationService.stub(IPathService, new TestPathService(HOME_DIR)); + contribution = store.add(instantiationService.createInstance(SessionsTerminalContribution)); }); @@ -120,11 +127,11 @@ suite('SessionsTerminalContribution', () => { ensureNoDisposablesAreLeakedInTestSuite(); - // --- getSessionCwd logic (via active session changes) --- + // --- Background provider: uses worktree/repository path --- - test('creates a terminal when active session has a worktree (non-cloud agent)', async () => { + test('creates a terminal at the worktree for a background session', async () => { const worktreeUri = URI.file('/worktree'); - const session = makeAgentSession({ worktree: worktreeUri, repository: URI.file('/repo'), providerType: AgentSessionProviders.Local }); + const session = makeAgentSession({ worktree: worktreeUri, repository: URI.file('/repo'), providerType: AgentSessionProviders.Background }); activeSessionObs.set(session, undefined); await tick(); @@ -132,59 +139,89 @@ suite('SessionsTerminalContribution', () => { assert.strictEqual(createdTerminals[0].cwd.fsPath, worktreeUri.fsPath); }); - test('reate a terminal with repository for cloud agent sessions', async () => { + test('falls back to repository when worktree is undefined for a background session', async () => { const repoUri = URI.file('/repo'); - const workTree = URI.file('/worktree'); - const session = makeAgentSession({ worktree: workTree, repository: repoUri, providerType: AgentSessionProviders.Cloud }); + const session = makeAgentSession({ repository: repoUri, providerType: AgentSessionProviders.Background }); activeSessionObs.set(session, undefined); await tick(); assert.strictEqual(createdTerminals.length, 1); - assert.strictEqual(createdTerminals[0].cwd.fsPath, workTree.fsPath); + assert.strictEqual(createdTerminals[0].cwd.fsPath, repoUri.fsPath); }); - test('creates a terminal with repository for non-agent sessions', async () => { - const repoUri = URI.file('/repo'); - const session = makeNonAgentSession({ repository: repoUri }); + // --- Non-background providers: use home directory --- + + test('uses home directory for a cloud agent session', async () => { + const session = makeAgentSession({ worktree: URI.file('/worktree'), repository: URI.file('/repo'), providerType: AgentSessionProviders.Cloud }); activeSessionObs.set(session, undefined); await tick(); assert.strictEqual(createdTerminals.length, 1); - assert.strictEqual(createdTerminals[0].cwd.fsPath, repoUri.fsPath); + assert.strictEqual(createdTerminals[0].cwd.fsPath, HOME_DIR.fsPath); }); - test('does not create a terminal when no path is available', async () => { - const session = makeNonAgentSession({}); + test('uses home directory for a local agent session', async () => { + const session = makeAgentSession({ worktree: URI.file('/worktree'), providerType: AgentSessionProviders.Local }); activeSessionObs.set(session, undefined); await tick(); + assert.strictEqual(createdTerminals.length, 1); + assert.strictEqual(createdTerminals[0].cwd.fsPath, HOME_DIR.fsPath); + }); + + test('uses home directory for a non-agent session', async () => { + const session = makeNonAgentSession({ repository: URI.file('/repo') }); + activeSessionObs.set(session, undefined); + await tick(); + + assert.strictEqual(createdTerminals.length, 1); + assert.strictEqual(createdTerminals[0].cwd.fsPath, HOME_DIR.fsPath); + }); + + test('does not recreate terminal when multiple non-background sessions share the home directory', async () => { + const session1 = makeAgentSession({ providerType: AgentSessionProviders.Cloud }); + activeSessionObs.set(session1, undefined); + await tick(); + assert.strictEqual(createdTerminals.length, 1); + + // Different non-background session — same home dir, no new terminal + const session2 = makeAgentSession({ providerType: AgentSessionProviders.Local }); + activeSessionObs.set(session2, undefined); + await tick(); + assert.strictEqual(createdTerminals.length, 1); + }); + + test('does not create a terminal when there is no active session', async () => { + activeSessionObs.set(undefined, undefined); + await tick(); + assert.strictEqual(createdTerminals.length, 0); }); test('does not recreate terminal for the same path', async () => { const worktreeUri = URI.file('/worktree'); - const session1 = makeAgentSession({ worktree: worktreeUri, providerType: AgentSessionProviders.Local }); + const session1 = makeAgentSession({ worktree: worktreeUri, providerType: AgentSessionProviders.Background }); activeSessionObs.set(session1, undefined); await tick(); assert.strictEqual(createdTerminals.length, 1); // Setting a different session with the same worktree should not create a new terminal - const session2 = makeAgentSession({ worktree: worktreeUri, providerType: AgentSessionProviders.Local }); + const session2 = makeAgentSession({ worktree: worktreeUri, providerType: AgentSessionProviders.Background }); activeSessionObs.set(session2, undefined); await tick(); assert.strictEqual(createdTerminals.length, 1); }); - test('creates new terminal when switching to a different path', async () => { + test('creates new terminal when switching to a different background path', async () => { const worktree1 = URI.file('/worktree1'); const worktree2 = URI.file('/worktree2'); - activeSessionObs.set(makeAgentSession({ worktree: worktree1, providerType: AgentSessionProviders.Local }), undefined); + activeSessionObs.set(makeAgentSession({ worktree: worktree1, providerType: AgentSessionProviders.Background }), undefined); await tick(); - activeSessionObs.set(makeAgentSession({ worktree: worktree2, providerType: AgentSessionProviders.Local }), undefined); + activeSessionObs.set(makeAgentSession({ worktree: worktree2, providerType: AgentSessionProviders.Background }), undefined); await tick(); assert.strictEqual(createdTerminals.length, 2); @@ -290,64 +327,22 @@ suite('SessionsTerminalContribution', () => { assert.strictEqual(createdTerminals.length, 2, 'should create a new terminal after the old one was disposed'); }); - // --- agent session with worktree preferred over repository for non-cloud --- - - test('prefers worktree over repository for local agent session', async () => { - const worktreeUri = URI.file('/worktree'); - const repoUri = URI.file('/repo'); - const session = makeAgentSession({ - worktree: worktreeUri, - repository: repoUri, - providerType: AgentSessionProviders.Local, - }); - activeSessionObs.set(session, undefined); - await tick(); - - assert.strictEqual(createdTerminals[0].cwd.fsPath, worktreeUri.fsPath); - }); - - test('falls back to repository when worktree is undefined for agent session', async () => { - const repoUri = URI.file('/repo'); - const session = makeAgentSession({ - repository: repoUri, - providerType: AgentSessionProviders.Local, - }); - activeSessionObs.set(session, undefined); - await tick(); - - assert.strictEqual(createdTerminals[0].cwd.fsPath, repoUri.fsPath); - }); - - test('does not use repository for cloud agent session when worktree exists', async () => { - const worktreeUri = URI.file('/worktree'); - const repoUri = URI.file('/repo'); - const session = makeAgentSession({ - worktree: worktreeUri, - repository: repoUri, - providerType: AgentSessionProviders.Cloud, - }); - activeSessionObs.set(session, undefined); - await tick(); - - assert.strictEqual(createdTerminals[0].cwd.fsPath, worktreeUri.fsPath); - }); - // --- switching back to previously used path reuses terminal --- - test('switching back to a previously used path reuses the existing terminal', async () => { + test('switching back to a previously used background path reuses the existing terminal', async () => { const cwd1 = URI.file('/cwd1'); const cwd2 = URI.file('/cwd2'); - activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Local }), undefined); + activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Background }), undefined); await tick(); assert.strictEqual(createdTerminals.length, 1); - activeSessionObs.set(makeAgentSession({ worktree: cwd2, providerType: AgentSessionProviders.Local }), undefined); + activeSessionObs.set(makeAgentSession({ worktree: cwd2, providerType: AgentSessionProviders.Background }), undefined); await tick(); assert.strictEqual(createdTerminals.length, 2); // Switch back to cwd1 - should reuse terminal, not create a new one - activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Local }), undefined); + activeSessionObs.set(makeAgentSession({ worktree: cwd1, providerType: AgentSessionProviders.Background }), undefined); await tick(); assert.strictEqual(createdTerminals.length, 2, 'should reuse the terminal for cwd1'); }); diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css b/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css index 015f11afd5bde..9f11489cc35d8 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsCenter.css @@ -10,7 +10,8 @@ bottom: 29px; /* 22px status bar height + 7px (attempt to position at same location as a toast) */ display: none; overflow: hidden; - border-radius: 4px; + border: 1px solid var(--vscode-editorWidget-border); + border-radius: var(--vscode-cornerRadius-small); } .monaco-workbench.nostatusbar > .notifications-center { @@ -59,7 +60,7 @@ } .monaco-workbench > .notifications-center .notifications-list-container .monaco-list-row:last-child { - border-radius: 0px 0px 4px 4px; /* adopt the border radius at the end of the notifications center */ + border-radius: 0px 0px var(--vscode-cornerRadius-small) var(--vscode-cornerRadius-small); /* adopt the border radius at the end of the notifications center */ } /* Icons */ diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css b/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css index 0f5f3fdbfbab1..af90372abdc83 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css @@ -10,6 +10,8 @@ bottom: 25px; /* 22px status bar height + 3px */ display: none; overflow: hidden; + box-shadow: 0 0 12px var(--vscode-widget-shadow); + border-radius: var(--vscode-cornerRadius-small); } .monaco-workbench.nostatusbar > .notifications-toasts { diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 454939c8afc4a..cf0977721d589 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -172,7 +172,7 @@ border: 1px solid var(--vscode-commandCenter-border); overflow: hidden; margin: 0 6px; - border-radius: 4px; + border-radius: var(--vscode-cornerRadius-large); height: 22px; width: 38vw; max-width: 600px; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 7516c88f8edf3..98fb8e824f961 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -85,6 +85,7 @@ export const GENERATE_PROMPT_COMMAND_ID = 'workbench.action.chat.generatePrompt' export const GENERATE_SKILL_COMMAND_ID = 'workbench.action.chat.generateSkill'; export const GENERATE_AGENT_COMMAND_ID = 'workbench.action.chat.generateAgent'; export const GENERATE_HOOK_COMMAND_ID = 'workbench.action.chat.generateHook'; +export const INSERT_FORK_CONVERSATION_COMMAND_ID = 'workbench.action.chat.insertForkConversationCommand'; const defaultChat = { manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', @@ -1370,6 +1371,28 @@ export function registerChatActions() { } }); + registerAction2(class InsertForkConversationSlashCommandAction extends Action2 { + constructor() { + super({ + id: INSERT_FORK_CONVERSATION_COMMAND_ID, + title: localize2('insertForkConversationSlashCommand', "Insert Fork Command"), + shortTitle: localize2('insertForkConversationSlashCommand.short', "Insert /fork"), + category: CHAT_CATEGORY, + icon: Codicon.repoForked, + f1: true, + precondition: ChatContextKeys.enabled + }); + } + + async run(accessor: ServicesAccessor): Promise { + const commandService = accessor.get(ICommandService); + await commandService.executeCommand('workbench.action.chat.open', { + query: '/fork ', + isPartialQuery: true, + }); + } + }); + registerAction2(class OpenChatFeatureSettingsAction extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 218dad8cb8f26..2d40294cfeb36 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -22,6 +22,7 @@ import { IProductService } from '../../../../../platform/product/common/productS import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { IWorkspaceTrustManagementService } from '../../../../../platform/workspace/common/workspaceTrust.js'; import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; import { Extensions, IOutputChannelRegistry, IOutputService } from '../../../../services/output/common/output.js'; @@ -398,6 +399,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode @IProductService private readonly productService: IProductService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, ) { super(); @@ -426,19 +428,12 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode private registerListeners(): void { - // Sessions changes - this._register(this.chatSessionsService.onDidChangeItemsProviders(({ chatSessionType }) => { - this.resolve(chatSessionType); - })); - this._register(this.chatSessionsService.onDidChangeAvailability(() => { - this.resolve(undefined); - })); - this._register(this.chatSessionsService.onDidChangeSessionItems(({ chatSessionType }) => { - this.updateItems([chatSessionType], CancellationToken.None); - })); - this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => { - this.resolve(undefined); - })); + // Sessions updates + this._register(this.chatSessionsService.onDidChangeItemsProviders(({ chatSessionType }) => this.resolve(chatSessionType))); + this._register(this.chatSessionsService.onDidChangeAvailability(() => this.resolve(undefined))); + this._register(this.chatSessionsService.onDidChangeSessionItems(({ chatSessionType }) => this.updateItems([chatSessionType], CancellationToken.None))); + this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => this.resolve(undefined))); + this._register(this.workspaceTrustManagementService.onDidChangeTrust(() => this.resolve(undefined))); // State this._register(this.storageService.onWillSaveState(() => { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 2c3b010411927..569411deae595 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -53,7 +53,7 @@ import { ILanguageModelToolsService } from '../common/tools/languageModelToolsSe import { agentPluginDiscoveryRegistry, IAgentPluginService } from '../common/plugins/agentPluginService.js'; import { ChatPromptFilesExtensionPointHandler } from '../common/promptSyntax/chatPromptFilesContribution.js'; import { PromptsConfig } from '../common/promptSyntax/config/config.js'; -import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION, DEFAULT_SKILL_SOURCE_FOLDERS, AGENTS_SOURCE_FOLDER, AGENT_FILE_EXTENSION, SKILL_FILENAME, CLAUDE_AGENTS_SOURCE_FOLDER, DEFAULT_HOOK_FILE_PATHS, DEFAULT_INSTRUCTIONS_SOURCE_FOLDERS, PromptFileSource } from '../common/promptSyntax/config/promptFileLocations.js'; +import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION, DEFAULT_SKILL_SOURCE_FOLDERS, AGENTS_SOURCE_FOLDER, AGENT_FILE_EXTENSION, SKILL_FILENAME, CLAUDE_AGENTS_SOURCE_FOLDER, DEFAULT_HOOK_FILE_PATHS, DEFAULT_INSTRUCTIONS_SOURCE_FOLDERS, PromptFileSource, COPILOT_USER_AGENTS_SOURCE_FOLDER } from '../common/promptSyntax/config/promptFileLocations.js'; import { PromptLanguageFeaturesProvider } from '../common/promptSyntax/promptFileContributions.js'; import { AGENT_DOCUMENTATION_URL, INSTRUCTIONS_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL, SKILL_DOCUMENTATION_URL, HOOK_DOCUMENTATION_URL, PromptsType } from '../common/promptSyntax/promptTypes.js'; import { hookFileSchema, HOOK_SCHEMA_URI } from '../common/promptSyntax/hookSchema.js'; @@ -879,6 +879,7 @@ configurationRegistry.registerConfiguration({ default: { [AGENTS_SOURCE_FOLDER]: true, [CLAUDE_AGENTS_SOURCE_FOLDER]: true, + [COPILOT_USER_AGENTS_SOURCE_FOLDER]: true, }, additionalProperties: { type: 'boolean' }, propertyNames: { diff --git a/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts b/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts index d130efd72758d..b29cc69ac81da 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipCatalog.ts @@ -18,6 +18,7 @@ import { GENERATE_ON_DEMAND_INSTRUCTIONS_COMMAND_ID, GENERATE_PROMPT_COMMAND_ID, GENERATE_SKILL_COMMAND_ID, + INSERT_FORK_CONVERSATION_COMMAND_ID, } from './actions/chatActions.js'; /** @@ -287,6 +288,26 @@ export const TIP_CATALOG: readonly ITipDefinition[] = [ when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), excludeWhenCommandsExecuted: ['workbench.action.chat.queueMessage', 'workbench.action.chat.steerWithMessage'], }, + { + id: 'tip.forkConversation', + buildMessage(ctx) { + const kb = formatKeybinding(ctx, INSERT_FORK_CONVERSATION_COMMAND_ID); + return new MarkdownString( + localize( + 'tip.forkConversation', + "Use [{0}](command:{1}){2} to branch the conversation. Explore a different approach without losing the original context.", + '/fork', + INSERT_FORK_CONVERSATION_COMMAND_ID, + kb + ) + ); + }, + excludeWhenCommandsExecuted: [ + INSERT_FORK_CONVERSATION_COMMAND_ID, + 'workbench.action.chat.forkConversation', + TipTrackingCommands.ForkConversationUsed, + ], + }, { id: 'tip.yoloMode', buildMessage() { diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index 3bc000338552c..57ee53a820f0e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -54,6 +54,8 @@ export const CREATE_PROMPT_TRACKING_COMMAND = TipTrackingCommands.CreatePromptUs export const CREATE_AGENT_TRACKING_COMMAND = TipTrackingCommands.CreateAgentUsed; /** @deprecated Use TipTrackingCommands.CreateSkillUsed */ export const CREATE_SKILL_TRACKING_COMMAND = TipTrackingCommands.CreateSkillUsed; +/** @deprecated Use TipTrackingCommands.ForkConversationUsed */ +export const FORK_CONVERSATION_TRACKING_COMMAND = TipTrackingCommands.ForkConversationUsed; export const IChatTipService = createDecorator('chatTipService'); @@ -216,9 +218,9 @@ export class ChatTipService extends Disposable implements IChatTipService { this._tracker.recordCommandExecuted(TipTrackingCommands.AttachFilesReferenceUsed); } - const createCommandTrackingId = this._getCreateSlashCommandTrackingId(message); - if (createCommandTrackingId) { - this._tracker.recordCommandExecuted(createCommandTrackingId); + const slashCommandTrackingId = this._getSlashCommandTrackingId(message); + if (slashCommandTrackingId) { + this._tracker.recordCommandExecuted(slashCommandTrackingId); } })); @@ -267,20 +269,20 @@ export class ChatTipService extends Disposable implements IChatTipService { }); } - private _getCreateSlashCommandTrackingId(message: IParsedChatRequest): string | undefined { + private _getSlashCommandTrackingId(message: IParsedChatRequest): string | undefined { for (const part of message.parts) { if (part.kind === ChatRequestSlashCommandPart.Kind) { const slashCommand = (part as ChatRequestSlashCommandPart).slashCommand.command; - return this._toCreateSlashCommandTrackingId(slashCommand); + return this._toSlashCommandTrackingId(slashCommand); } } const trimmed = message.text.trimStart(); - const match = /^\/(create-(?:instructions|prompt|agent|skill))(?:\s|$)/.exec(trimmed); - return match ? this._toCreateSlashCommandTrackingId(match[1]) : undefined; + const match = /^\/(create-(?:instructions|prompt|agent|skill)|fork)(?:\s|$)/.exec(trimmed); + return match ? this._toSlashCommandTrackingId(match[1]) : undefined; } - private _toCreateSlashCommandTrackingId(command: string): string | undefined { + private _toSlashCommandTrackingId(command: string): string | undefined { switch (command) { case 'create-instructions': return CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND; @@ -290,6 +292,8 @@ export class ChatTipService extends Disposable implements IChatTipService { return CREATE_AGENT_TRACKING_COMMAND; case 'create-skill': return CREATE_SKILL_TRACKING_COMMAND; + case 'fork': + return FORK_CONVERSATION_TRACKING_COMMAND; default: return undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/chatTipStorageKeys.ts b/src/vs/workbench/contrib/chat/browser/chatTipStorageKeys.ts index 1b7a995f8c86b..f7ccacdd94ccc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipStorageKeys.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipStorageKeys.ts @@ -45,4 +45,6 @@ export const TipTrackingCommands = { CreateAgentUsed: 'chat.tips.createAgent.commandUsed', /** Tracked when user executes /create-skill. */ CreateSkillUsed: 'chat.tips.createSkill.commandUsed', + /** Tracked when user executes /fork. */ + ForkConversationUsed: 'chat.tips.forkConversation.commandUsed', } as const; diff --git a/src/vs/workbench/contrib/chat/browser/chatWindowNotifier.ts b/src/vs/workbench/contrib/chat/browser/chatWindowNotifier.ts index d9fe036dac880..3dce99f35720d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWindowNotifier.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWindowNotifier.ts @@ -11,17 +11,14 @@ import { autorunDelta, autorunIterableDelta } from '../../../../base/common/obse import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { FocusMode } from '../../../../platform/native/common/native.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IHostService } from '../../../services/host/browser/host.js'; import { IChatModel, IChatRequestNeedsInputInfo } from '../common/model/chatModel.js'; -import { IChatService } from '../common/chatService/chatService.js'; +import { IChatService, IChatToolInvocation, ToolConfirmKind } from '../common/chatService/chatService.js'; import { migrateLegacyTerminalToolSpecificData } from '../common/chat.js'; import { ChatConfiguration, ChatNotificationMode } from '../common/constants.js'; import { IChatWidgetService } from './chat.js'; -import { AcceptToolConfirmationActionId, IToolConfirmationActionContext } from './actions/chatToolActions.js'; -import { isMacintosh } from '../../../../base/common/platform.js'; /** * Observes all live chat models and triggers OS notifications when any model @@ -38,7 +35,6 @@ export class ChatWindowNotifier extends Disposable implements IWorkbenchContribu @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IHostService private readonly _hostService: IHostService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @ICommandService private readonly _commandService: ICommandService, ) { super(); @@ -116,28 +112,44 @@ export class ChatWindowNotifier extends Disposable implements IWorkbenchContribu actions: [actionLabel], }, cts.token); + if (result.actionIndex === 0 && !isQuestionCarousel && this._confirmAllow(sessionResource)) { + return; // skip focusing/opening chat if we successfully confirmed the tool invocation from the toast action + } + if (result.clicked || typeof result.actionIndex === 'number') { await this._hostService.focus(targetWindow, { mode: FocusMode.Force }); const widget = await this._chatWidgetService.openSession(sessionResource); widget?.focusInput(); - - if (result.actionIndex === 0 && !isQuestionCarousel) { - await this._commandService.executeCommand(AcceptToolConfirmationActionId, { sessionResource } satisfies IToolConfirmationActionContext); - } } } finally { this._clearNotification(sessionResource); } } + private _confirmAllow(sessionResource: URI): boolean { + const model = this._chatService.getSession(sessionResource); + const lastResponse = model?.lastRequest?.response; + if (!lastResponse) { + return false; + } + for (const part of lastResponse.response.value) { + const state = part.kind === 'toolInvocation' ? part.state.get() : undefined; + if (state?.type === IChatToolInvocation.StateKind.WaitingForConfirmation || state?.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { + state.confirm({ type: ToolConfirmKind.UserAction }); + return true; + } + } + return false; + } + private _getNotificationBody(sessionResource: URI, info: IChatRequestNeedsInputInfo, isQuestionCarousel: boolean): string { - const terminalCommand = this._getPendingTerminalCommand(sessionResource); if (isQuestionCarousel) { return localize('questionCarouselDetail', "Questions need your input."); } - if (isMacintosh && terminalCommand) { - return this._sanitizeOSToastText(terminalCommand); // prefer full command on macOS where you can approve from the toast + const terminalCommand = this._getPendingTerminalCommand(sessionResource); + if (terminalCommand) { + return this._sanitizeOSToastText(terminalCommand); } if (info.detail) { return this._sanitizeOSToastText(info.detail); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts index 70de863c8f815..779974e1e6310 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts @@ -86,6 +86,11 @@ export const AGENTS_SOURCE_FOLDER = '.github/agents'; */ export const CLAUDE_AGENTS_SOURCE_FOLDER = '.claude/agents'; +/** + * Copilot user agents folder. + */ +export const COPILOT_USER_AGENTS_SOURCE_FOLDER = '~/.copilot/agents'; + /** * Claude rules folder. */ @@ -186,6 +191,7 @@ export const DEFAULT_AGENT_SOURCE_FOLDERS: readonly IPromptSourceFolder[] = [ { path: AGENTS_SOURCE_FOLDER, source: PromptFileSource.GitHubWorkspace, storage: PromptsStorage.local }, { path: CLAUDE_AGENTS_SOURCE_FOLDER, source: PromptFileSource.ClaudeWorkspace, storage: PromptsStorage.local }, { path: '~/' + CLAUDE_AGENTS_SOURCE_FOLDER, source: PromptFileSource.ClaudePersonal, storage: PromptsStorage.user }, + { path: COPILOT_USER_AGENTS_SOURCE_FOLDER, source: PromptFileSource.CopilotPersonal, storage: PromptsStorage.user }, ]; /** @@ -204,7 +210,7 @@ export const DEFAULT_HOOK_FILE_PATHS: readonly IPromptSourceFolder[] = [ */ function isInAgentsFolder(fileUri: URI): boolean { const dir = dirname(fileUri.path); - return dir.endsWith('/' + AGENTS_SOURCE_FOLDER) || dir.endsWith('/' + CLAUDE_AGENTS_SOURCE_FOLDER); + return dir.endsWith('/' + AGENTS_SOURCE_FOLDER) || dir.endsWith('/' + CLAUDE_AGENTS_SOURCE_FOLDER) || isInCopilotAgentsFolder(fileUri); } /** @@ -215,6 +221,14 @@ export function isInClaudeAgentsFolder(fileUri: URI): boolean { return dir.endsWith('/' + CLAUDE_AGENTS_SOURCE_FOLDER); } +/** + * Helper function to check if a file is directly in the ~/.copilot/agents/ folder. + */ +export function isInCopilotAgentsFolder(fileUri: URI): boolean { + const dir = dirname(fileUri.path); + return dir.endsWith(COPILOT_USER_AGENTS_SOURCE_FOLDER.substring(1)); +} + /** * Helper function to check if a file is inside the .claude/rules/ folder (including subfolders). * Claude rules files (.md) in this folder are treated as instruction files. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index a6058e2c47ec7..7d05dd9ff61fd 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -23,10 +23,9 @@ import { ResourceMap } from '../../../../../../base/common/map.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; import { IPromptsService, Target } from '../service/promptsService.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; -import { AGENTS_SOURCE_FOLDER, CLAUDE_AGENTS_SOURCE_FOLDER, isInClaudeRulesFolder, LEGACY_MODE_FILE_EXTENSION } from '../config/promptFileLocations.js'; +import { AGENTS_SOURCE_FOLDER, isInClaudeAgentsFolder, isInClaudeRulesFolder, isInCopilotAgentsFolder, LEGACY_MODE_FILE_EXTENSION } from '../config/promptFileLocations.js'; import { Lazy } from '../../../../../../base/common/lazy.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { dirname } from '../../../../../../base/common/resources.js'; import { URI } from '../../../../../../base/common/uri.js'; export const MARKERS_OWNER_ID = 'prompts-diagnostics-provider'; @@ -1023,10 +1022,12 @@ export function isVSCodeOrDefaultTarget(target: Target): boolean { export function getTarget(promptType: PromptsType, header: PromptHeader | URI): Target { const uri = header instanceof URI ? header : header.uri; if (promptType === PromptsType.agent) { - const parentDir = dirname(uri); - if (parentDir.path.endsWith(`/${CLAUDE_AGENTS_SOURCE_FOLDER}`)) { + if (isInClaudeAgentsFolder(uri)) { return Target.Claude; } + if (isInCopilotAgentsFolder(uri)) { + return Target.GitHubCopilot; + } if (!(header instanceof URI)) { const target = header.target; if (target === Target.GitHubCopilot || target === Target.VSCode) { diff --git a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts index ba43aa820ee2c..f6e5316b0bb0b 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts @@ -17,7 +17,7 @@ import { MockContextKeyService } from '../../../../../platform/keybinding/test/c import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; import { IStorageService, InMemoryStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; -import { ChatTipService, CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND, CREATE_AGENT_TRACKING_COMMAND, CREATE_PROMPT_TRACKING_COMMAND, CREATE_SKILL_TRACKING_COMMAND, IChatTip, ITipDefinition, TipEligibilityTracker } from '../../browser/chatTipService.js'; +import { ChatTipService, CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND, CREATE_AGENT_TRACKING_COMMAND, CREATE_PROMPT_TRACKING_COMMAND, CREATE_SKILL_TRACKING_COMMAND, FORK_CONVERSATION_TRACKING_COMMAND, IChatTip, ITipDefinition, TipEligibilityTracker } from '../../browser/chatTipService.js'; import { AgentFileType, IPromptPath, IPromptsService, IResolvedAgentFile, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { URI } from '../../../../../base/common/uri.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; @@ -189,6 +189,32 @@ suite('ChatTipService', () => { assert.ok(!executedCommands.includes(CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND)); assert.ok(!executedCommands.includes(CREATE_AGENT_TRACKING_COMMAND)); assert.ok(!executedCommands.includes(CREATE_SKILL_TRACKING_COMMAND)); + assert.ok(!executedCommands.includes(FORK_CONVERSATION_TRACKING_COMMAND)); + }); + + test('records fork tip usage for submitted /fork command', () => { + const submitRequestEmitter = testDisposables.add(new Emitter<{ readonly chatSessionResource: URI; readonly message?: IParsedChatRequest }>()); + instantiationService.stub(IChatService, { + onDidSubmitRequest: submitRequestEmitter.event, + getSession: () => undefined, + } as Partial as IChatService); + + createService(); + + submitRequestEmitter.fire({ + chatSessionResource: URI.parse('chat:session-fork'), + message: { + text: '/fork', + parts: [], + }, + }); + + const executedCommands = JSON.parse(storageService.get('chat.tips.executedCommands', StorageScope.APPLICATION) ?? '[]') as string[]; + assert.ok(executedCommands.includes(FORK_CONVERSATION_TRACKING_COMMAND)); + assert.ok(!executedCommands.includes(CREATE_AGENT_INSTRUCTIONS_TRACKING_COMMAND)); + assert.ok(!executedCommands.includes(CREATE_PROMPT_TRACKING_COMMAND)); + assert.ok(!executedCommands.includes(CREATE_AGENT_TRACKING_COMMAND)); + assert.ok(!executedCommands.includes(CREATE_SKILL_TRACKING_COMMAND)); }); test('returns Auto switch tip when current model is gpt-4.1', () => { diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/config/promptFileLocations.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/config/promptFileLocations.test.ts index f6d859e9f2736..f7e22613dee3b 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/config/promptFileLocations.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/config/promptFileLocations.test.ts @@ -48,6 +48,36 @@ suite('promptFileLocations', function () { assert.strictEqual(getPromptFileType(uri), undefined); }); + test('.md files in .claude/agents/ subfolder should NOT be recognized as agent files', () => { + const uri = URI.file('/workspace/.claude/agents/subfolder/test.md'); + assert.strictEqual(getPromptFileType(uri), undefined); + }); + + test('.md files in ~/.copilot/agents/ subfolder should NOT be recognized as agent files', () => { + const uri = URI.file('/home/user/.copilot/agents/subfolder/test.md'); + assert.strictEqual(getPromptFileType(uri), undefined); + }); + + test('.md files in .claude/agents/ folder should be recognized as agent files', () => { + const uri = URI.file('/workspace/.claude/agents/demonstrate.md'); + assert.strictEqual(getPromptFileType(uri), PromptsType.agent); + }); + + test('README.md in .claude/agents/ should NOT be recognized as agent file', () => { + const uri = URI.file('/workspace/.claude/agents/README.md'); + assert.strictEqual(getPromptFileType(uri), undefined); + }); + + test('.md files in ~/.copilot/agents/ folder should be recognized as agent files', () => { + const uri = URI.file('/home/user/.copilot/agents/my-agent.md'); + assert.strictEqual(getPromptFileType(uri), PromptsType.agent); + }); + + test('README.md in ~/.copilot/agents/ should NOT be recognized as agent file', () => { + const uri = URI.file('/home/user/.copilot/agents/README.md'); + assert.strictEqual(getPromptFileType(uri), undefined); + }); + test('.md files outside .github/agents/ should not be recognized as agent files', () => { const uri = URI.file('/workspace/test/foo.md'); assert.strictEqual(getPromptFileType(uri), undefined); @@ -130,6 +160,16 @@ suite('promptFileLocations', function () { assert.strictEqual(getCleanPromptName(uri), 'demonstrate'); }); + test('removes .md extension for files in .claude/agents/', () => { + const uri = URI.file('/workspace/.claude/agents/claude-agent.md'); + assert.strictEqual(getCleanPromptName(uri), 'claude-agent'); + }); + + test('removes .md extension for files in ~/.copilot/agents/', () => { + const uri = URI.file('/home/user/.copilot/agents/my-agent.md'); + assert.strictEqual(getCleanPromptName(uri), 'my-agent'); + }); + test('README.md in .github/agents/ should keep .md extension', () => { const uri = URI.file('/workspace/.github/agents/README.md'); assert.strictEqual(getCleanPromptName(uri), 'README.md'); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index 90e6605a00dd3..0f3e39807dbe9 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -1177,6 +1177,56 @@ suite('PromptsService', () => { ); }); + test('copilot user agents from ~/.copilot/agents/ should have GitHubCopilot target', async () => { + const rootFolderName = 'copilot-user-agents'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + // Copilot user agent in ~/.copilot/agents/ (resolved from /home/user/.copilot/agents/) + path: '/home/user/.copilot/agents/copilot-user-agent.md', + contents: [ + '---', + 'description: \'Copilot user agent from home folder.\'', + 'tools: [ read ]', + '---', + 'I am a Copilot user agent.', + ] + }, + ]); + + const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const expected: ICustomAgent[] = [ + { + name: 'copilot-user-agent', + description: 'Copilot user agent from home folder.', + target: Target.GitHubCopilot, + tools: ['read'], + agentInstructions: { + content: 'I am a Copilot user agent.', + toolReferences: [], + metadata: undefined + }, + handOffs: undefined, + model: undefined, + argumentHint: undefined, + visibility: { userInvocable: true, agentInvocable: true }, + agents: undefined, + uri: URI.file('/home/user/.copilot/agents/copilot-user-agent.md'), + source: { storage: PromptsStorage.user } + }, + ]; + + assert.deepEqual( + result, + expected, + 'Agents from ~/.copilot/agents/ must have Target.GitHubCopilot.', + ); + }); + test('agents with .md extension should be recognized, except README.md', async () => { const rootFolderName = 'custom-agents-md-extension'; const rootFolder = `/${rootFolderName}`; diff --git a/src/vs/workbench/contrib/debug/browser/media/debugHover.css b/src/vs/workbench/contrib/debug/browser/media/debugHover.css index 3a862adbe9fdb..e7dd01a9cfbcc 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugHover.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugHover.css @@ -12,6 +12,7 @@ -webkit-user-select: text; word-break: break-all; white-space: pre; + border-radius: var(--vscode-cornerRadius-large); } .monaco-editor .debug-hover-widget .complex-value { diff --git a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css index 090c53e7f9844..f8a588049f092 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css @@ -9,7 +9,7 @@ height: 28px; display: flex; padding-left: 2px; - border-radius: 5px; + border-radius: var(--vscode-cornerRadius-large); left: 0; top: 0; -webkit-app-region: no-drag; diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index da1e670cf8bc7..5c7b116c2bf23 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -6,7 +6,7 @@ .monaco-workbench .inline-chat { color: inherit; - border-radius: 4px; + border-radius: var(--vscode-cornerRadius-large); border: 1px solid var(--vscode-inlineChat-border); box-shadow: 0 2px 4px 0 var(--vscode-widget-shadow); background: var(--vscode-inlineChat-background); diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css index 9acc9ecf81703..be58df0988b27 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css @@ -8,7 +8,7 @@ .inline-chat-gutter-menu { background: var(--vscode-panel-background); border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border)); - border-radius: 8px; + border-radius: var(--vscode-cornerRadius-large); box-shadow: 0 2px 8px var(--vscode-widget-shadow); z-index: 100; } diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index dff9db0ecce6f..54f1f0dfc782c 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -294,6 +294,7 @@ left: 0px; top: 0px; bottom: 0px; + border-radius: var(--vscode-cornerRadius-medium); outline-offset: -1px; display: block; position: absolute; @@ -375,6 +376,15 @@ .notebookOverlay .cell-drag-image .cell-editor-container > div { background: var(--vscode-editor-background) !important; } + +.notebookOverlay .cell-bottom-toolbar-container .action-item { + border-radius: var(--vscode-cornerRadius-small); +} + +.notebookOverlay .monaco-list-row .cell-title-toolbar { + border-radius: var(--vscode-cornerRadius-medium); +} + .notebookOverlay .monaco-list-row .cell-title-toolbar, .notebookOverlay .monaco-list-row.cell-drag-image, .notebookOverlay .cell-bottom-toolbar-container .action-item, diff --git a/src/vs/workbench/contrib/notebook/browser/notebookExtensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/notebookExtensionPoint.ts index 6de1d23298e3f..e0c53ce067c5b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookExtensionPoint.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookExtensionPoint.ts @@ -158,7 +158,7 @@ const notebookRendererContribution: IJSONSchema = { ], enumDescriptions: [ nls.localize('contributes.notebook.renderer.requiresMessaging.always', 'Messaging is required. The renderer will only be used when it\'s part of an extension that can be run in an extension host.'), - nls.localize('contributes.notebook.renderer.requiresMessaging.optional', 'The renderer is better with messaging available, but it\'s not requried.'), + nls.localize('contributes.notebook.renderer.requiresMessaging.optional', 'The renderer is better with messaging available, but it\'s not required.'), nls.localize('contributes.notebook.renderer.requiresMessaging.never', 'The renderer does not require messaging.'), ], description: nls.localize('contributes.notebook.renderer.requiresMessaging', 'Defines how and if the renderer needs to communicate with an extension host, via `createRendererMessaging`. Renderers with stronger messaging requirements may not work in all environments.'), diff --git a/src/vs/workbench/contrib/preferences/browser/media/keybindings.css b/src/vs/workbench/contrib/preferences/browser/media/keybindings.css index e77553529e87f..3874b5b70f795 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/keybindings.css +++ b/src/vs/workbench/contrib/preferences/browser/media/keybindings.css @@ -5,6 +5,7 @@ .defineKeybindingWidget { padding: 10px; + border-radius: var(--vscode-cornerRadius-large); position: absolute; } diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 9370b48b871f6..58400159f85ce 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -63,7 +63,7 @@ } .monaco-workbench .terminal-editor .terminal-wrapper { - background-color: var(--vscode-terminal-background, var(--vscode-editorPane-background)); + background-color: var(--vscode-editor-background); } .monaco-workbench .terminal-editor .terminal-wrapper, .monaco-workbench .pane-body.integrated-terminal .terminal-wrapper { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 79023c406f44a..4d881d9cb1b23 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -1671,7 +1671,7 @@ async function pickTerminalCwd(accessor: ServicesAccessor, cancel?: Cancellation const folderPicks: Item[] = shrinkedPairs.map(pair => { const label = pair.folder.name; const description = pair.isOverridden - ? localize('workbench.action.terminal.overriddenCwdDescription', "(Overriden) {0}", labelService.getUriLabel(pair.cwd, { relative: !pair.isAbsolute })) + ? localize('workbench.action.terminal.overriddenCwdDescription', "(Overridden) {0}", labelService.getUriLabel(pair.cwd, { relative: !pair.isAbsolute })) : labelService.getUriLabel(dirname(pair.cwd), { relative: true }); return { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index f78a087fbd854..93a50146bc1a9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -2812,13 +2812,13 @@ export class TerminalInstanceColorProvider implements IXtermColorProvider { } getBackgroundColor(theme: IColorTheme) { + if (this._target.object === TerminalLocation.Editor) { + return theme.getColor(editorBackground); + } const terminalBackground = theme.getColor(TERMINAL_BACKGROUND_COLOR); if (terminalBackground) { return terminalBackground; } - if (this._target.object === TerminalLocation.Editor) { - return theme.getColor(editorBackground); - } const location = this._viewDescriptorService.getViewLocationById(TERMINAL_VIEW_ID)!; if (location === ViewContainerLocation.Panel) { return theme.getColor(PANEL_BACKGROUND); diff --git a/src/vs/workbench/contrib/terminal/electron-browser/localTerminalBackend.ts b/src/vs/workbench/contrib/terminal/electron-browser/localTerminalBackend.ts index 4435552b283f7..e30e2c69b465e 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/localTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/localTerminalBackend.ts @@ -37,6 +37,7 @@ import { IStatusbarService } from '../../../services/statusbar/browser/statusbar import { memoize } from '../../../../base/common/decorators.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; +import { INativeWorkbenchEnvironmentService } from '../../../services/environment/electron-browser/environmentService.js'; import { shouldUseEnvironmentVariableCollection } from '../../../../platform/terminal/common/terminalEnvironment.js'; import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; @@ -95,6 +96,7 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke @INativeHostService private readonly _nativeHostService: INativeHostService, @IStatusbarService statusBarService: IStatusbarService, @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, + @INativeWorkbenchEnvironmentService private readonly _environmentService: INativeWorkbenchEnvironmentService, ) { super(_localPtyService, logService, historyService, _configurationResolverService, statusBarService, workspaceContextService); @@ -294,7 +296,14 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke @memoize async getShellEnvironment(): Promise { - return this._shellEnvironmentService.getShellEnv(); + const env = { ... await this._shellEnvironmentService.getShellEnv() }; + + // If running in the context of an extension development host, include the environment derived from the launch configuration + if (this._environmentService.debugExtensionHost.env) { + terminalEnvironment.mergeEnvironments(env, this._environmentService.debugExtensionHost.env); + } + + return env; } async getWslPath(original: string, direction: 'unix-to-win' | 'win-to-unix'): Promise { diff --git a/test/sanity/src/index.ts b/test/sanity/src/index.ts index a251128892529..60fc2cab5a0ed 100644 --- a/test/sanity/src/index.ts +++ b/test/sanity/src/index.ts @@ -52,8 +52,22 @@ if (testResults) { const mocha = new Mocha(mochaOptions); mocha.addFile(fileURLToPath(new URL('./main.js', import.meta.url))); await mocha.loadFilesAsync(); -mocha.run(failures => { +const runner = mocha.run(failures => { + if (options.verbose) { + console.log(`Mocha test run finished: ${failures} failure(s)`); + } process.exitCode = failures > 0 ? 1 : 0; // Force exit to prevent hanging on open handles (background processes, timers, etc.) - setTimeout(() => process.exit(process.exitCode), 1000); + setTimeout(() => { + if (options.verbose) { + console.log(`Exiting with code ${process.exitCode}`); + } + process.exit(process.exitCode); + }, 1000); }); + +if (options.verbose) { + runner.on('test', (test) => console.log(`Starting: ${test.fullTitle()}`)); + runner.on('pass', (test) => console.log(`Passed: ${test.fullTitle()}`)); + runner.on('fail', (test, err) => console.log(`Failed: ${test.fullTitle()} - ${err.message}`)); +}