diff --git a/extensions/git/package.json b/extensions/git/package.json index bb18ee9bf6bba..47dbceb909276 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -321,6 +321,13 @@ "icon": "$(discard)", "enablement": "!operationInProgress" }, + { + "command": "git.delete", + "title": "%command.delete%", + "category": "Git", + "icon": "$(trash)", + "enablement": "!operationInProgress" + }, { "command": "git.commit", "title": "%command.commit%", @@ -1297,6 +1304,10 @@ "command": "git.rename", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == file && scmActiveResourceRepository" }, + { + "command": "git.delete", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == file" + }, { "command": "git.commit", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" @@ -3303,6 +3314,11 @@ "description": "%config.confirmSync%", "default": true }, + "git.confirmCommittedDelete": { + "type": "boolean", + "description": "%config.confirmCommittedDelete%", + "default": true + }, "git.countBadge": { "type": "string", "enum": [ diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index ef41d0d6f4448..94a1f61a51611 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -36,6 +36,7 @@ "command.unstageChange": "Unstage Change", "command.unstageSelectedRanges": "Unstage Selected Ranges", "command.rename": "Rename", + "command.delete": "Delete", "command.clean": "Discard Changes", "command.cleanAll": "Discard All Changes", "command.cleanAllTracked": "Discard All Tracked Changes", @@ -167,6 +168,7 @@ "config.autofetch": "When set to true, commits will automatically be fetched from the default remote of the current Git repository. Setting to `all` will fetch from all remotes.", "config.autofetchPeriod": "Duration in seconds between each automatic git fetch, when `#git.autofetch#` is enabled.", "config.confirmSync": "Confirm before synchronizing Git repositories.", + "config.confirmCommittedDelete": "Confirm before deleting committed files with Git.", "config.countBadge": "Controls the Git count badge.", "config.countBadge.all": "Count all changes.", "config.countBadge.tracked": "Count only tracked changes.", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index a0e362d56cf97..d531ac92e4978 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1405,6 +1405,41 @@ export class CommandCenter { await commands.executeCommand('vscode.open', Uri.file(path.join(repository.root, to)), { viewColumn: ViewColumn.Active }); } + @command('git.delete') + async delete(uri: Uri | undefined): Promise { + const activeDocument = window.activeTextEditor?.document; + uri = uri ?? activeDocument?.uri; + if (!uri) { + return; + } + + const repository = this.model.getRepository(uri); + if (!repository) { + return; + } + + const allChangedResources = [ + ...repository.workingTreeGroup.resourceStates, + ...repository.indexGroup.resourceStates, + ...repository.mergeGroup.resourceStates, + ...repository.untrackedGroup.resourceStates + ]; + + // Check if file has uncommitted changes + const uriString = uri.toString(); + if (allChangedResources.some(o => pathEquals(o.resourceUri.toString(), uriString))) { + window.showInformationMessage(l10n.t('Git: Delete can only be performed on committed files without uncommitted changes.')); + return; + } + + await repository.rm([uri]); + + // Close the active editor if it's not dirty + if (activeDocument && !activeDocument.isDirty && pathEquals(activeDocument.uri.toString(), uriString)) { + await commands.executeCommand('workbench.action.closeActiveEditor'); + } + } + @command('git.stage') async stage(...resourceStates: SourceControlResourceState[]): Promise { this.logger.debug(`[CommandCenter][stage] git.stage ${resourceStates.length} `); diff --git a/extensions/theme-2026/.vscodeignore b/extensions/theme-2026/.vscodeignore new file mode 100644 index 0000000000000..7ef29eaaabf89 --- /dev/null +++ b/extensions/theme-2026/.vscodeignore @@ -0,0 +1,5 @@ +CUSTOMIZATION.md +node_modules/** +.vscode/** +.gitignore +**/*.map diff --git a/extensions/theme-2026/cgmanifest.json b/extensions/theme-2026/cgmanifest.json new file mode 100644 index 0000000000000..74291602f069b --- /dev/null +++ b/extensions/theme-2026/cgmanifest.json @@ -0,0 +1,5 @@ +{ + "Version": 1, + "Registrations": [], + "Comments": [] +} diff --git a/extensions/theme-2026/package.json b/extensions/theme-2026/package.json new file mode 100644 index 0000000000000..b425a019e2fd5 --- /dev/null +++ b/extensions/theme-2026/package.json @@ -0,0 +1,38 @@ +{ + "name": "theme-2026", + "displayName": "2026 Themes", + "description": "Modern, minimal light and dark themes for 2026 with consistent neutral palette and accessible color contrast", + "version": "0.1.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "^1.85.0" + }, + "enabledApiProposals": [ + "css" + ], + "categories": [ + "Themes" + ], + "contributes": { + "themes": [ + { + "id": "2026-light-experimental", + "label": "2026 Light", + "uiTheme": "vs", + "path": "./themes/2026-light.json" + }, + { + "id": "2026-dark-experimental", + "label": "2026 Dark", + "uiTheme": "vs-dark", + "path": "./themes/2026-dark.json" + } + ], + "css": [ + { + "path": "./themes/styles.css" + } + ] + } +} diff --git a/extensions/theme-2026/package.nls.json b/extensions/theme-2026/package.nls.json new file mode 100644 index 0000000000000..639cf87f44e72 --- /dev/null +++ b/extensions/theme-2026/package.nls.json @@ -0,0 +1,6 @@ +{ + "displayName": "2026 Themes", + "description": "Modern, minimal light and dark themes for 2026 with consistent neutral palette and accessible color contrast", + "2026-light-label": "2026 Light", + "2026-dark-label": "2026 Dark" +} diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json new file mode 100644 index 0000000000000..5031dc271c715 --- /dev/null +++ b/extensions/theme-2026/themes/2026-dark.json @@ -0,0 +1,334 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "2026 Dark", + "type": "dark", + "colors": { + "foreground": "#bfbfbf", + "disabledForeground": "#444444", + "errorForeground": "#f48771", + "descriptionForeground": "#888888", + "icon.foreground": "#888888", + "focusBorder": "#498FADB3", + "textBlockQuote.background": "#232627", + "textBlockQuote.border": "#2A2B2CFF", + "textCodeBlock.background": "#232627", + "textLink.foreground": "#589BB8", + "textLink.activeForeground": "#61A0BC", + "textPreformat.foreground": "#888888", + "textSeparator.foreground": "#2a2a2aFF", + "button.background": "#498FAE", + "button.foreground": "#FFFFFF", + "button.hoverBackground": "#4D94B4", + "button.border": "#2A2B2CFF", + "button.secondaryBackground": "#232627", + "button.secondaryForeground": "#bfbfbf", + "button.secondaryHoverBackground": "#303234", + "checkbox.background": "#232627", + "checkbox.border": "#2A2B2CFF", + "checkbox.foreground": "#bfbfbf", + "dropdown.background": "#191B1D", + "dropdown.border": "#333536", + "dropdown.foreground": "#bfbfbf", + "dropdown.listBackground": "#1F2223", + "input.background": "#191B1D", + "input.border": "#333536FF", + "input.foreground": "#bfbfbf", + "input.placeholderForeground": "#777777", + "inputOption.activeBackground": "#498FAE33", + "inputOption.activeForeground": "#bfbfbf", + "inputOption.activeBorder": "#2A2B2CFF", + "inputValidation.errorBackground": "#191B1D", + "inputValidation.errorBorder": "#2A2B2CFF", + "inputValidation.errorForeground": "#bfbfbf", + "inputValidation.infoBackground": "#191B1D", + "inputValidation.infoBorder": "#2A2B2CFF", + "inputValidation.infoForeground": "#bfbfbf", + "inputValidation.warningBackground": "#191B1D", + "inputValidation.warningBorder": "#2A2B2CFF", + "inputValidation.warningForeground": "#bfbfbf", + "scrollbar.shadow": "#191B1D4D", + "scrollbarSlider.background": "#81848533", + "scrollbarSlider.hoverBackground": "#81848566", + "scrollbarSlider.activeBackground": "#81848599", + "badge.background": "#498FAE", + "badge.foreground": "#FFFFFF", + "progressBar.background": "#858889", + "list.activeSelectionBackground": "#498FAE26", + "list.activeSelectionForeground": "#bfbfbf", + "list.inactiveSelectionBackground": "#232627", + "list.inactiveSelectionForeground": "#bfbfbf", + "list.hoverBackground": "#252829", + "list.hoverForeground": "#bfbfbf", + "list.dropBackground": "#498FAE1A", + "list.focusBackground": "#498FAE26", + "list.focusForeground": "#bfbfbf", + "list.focusOutline": "#498FADB3", + "list.highlightForeground": "#bfbfbf", + "list.invalidItemForeground": "#444444", + "list.errorForeground": "#f48771", + "list.warningForeground": "#e5ba7d", + "activityBar.background": "#191B1D", + "activityBar.foreground": "#bfbfbf", + "activityBar.inactiveForeground": "#888888", + "activityBar.border": "#2A2B2CFF", + "activityBar.activeBorder": "#2A2B2CFF", + "activityBar.activeFocusBorder": "#498FADB3", + "activityBarBadge.background": "#498FAE", + "activityBarBadge.foreground": "#FFFFFF", + "sideBar.background": "#191B1D", + "sideBar.foreground": "#bfbfbf", + "sideBar.border": "#2A2B2CFF", + "sideBarTitle.foreground": "#bfbfbf", + "sideBarSectionHeader.background": "#191B1D", + "sideBarSectionHeader.foreground": "#bfbfbf", + "sideBarSectionHeader.border": "#2A2B2CFF", + "titleBar.activeBackground": "#191B1D", + "titleBar.activeForeground": "#bfbfbf", + "titleBar.inactiveBackground": "#191B1D", + "titleBar.inactiveForeground": "#888888", + "titleBar.border": "#2A2B2CFF", + "menubar.selectionBackground": "#232627", + "menubar.selectionForeground": "#bfbfbf", + "menu.background": "#1F2223", + "menu.foreground": "#bfbfbf", + "menu.selectionBackground": "#498FAE26", + "menu.selectionForeground": "#bfbfbf", + "menu.separatorBackground": "#818485", + "menu.border": "#2A2B2CFF", + "commandCenter.foreground": "#bfbfbf", + "commandCenter.activeForeground": "#bfbfbf", + "commandCenter.background": "#191B1D", + "commandCenter.activeBackground": "#252829", + "commandCenter.border": "#333536", + "editor.background": "#121416", + "editor.foreground": "#BBBEBF", + "editorLineNumber.foreground": "#858889", + "editorLineNumber.activeForeground": "#BBBEBF", + "editorCursor.foreground": "#BBBEBF", + "editor.selectionBackground": "#498FAE33", + "editor.inactiveSelectionBackground": "#498FAE80", + "editor.selectionHighlightBackground": "#498FAE1A", + "editor.wordHighlightBackground": "#498FAE33", + "editor.wordHighlightStrongBackground": "#498FAE33", + "editor.findMatchBackground": "#498FAE4D", + "editor.findMatchHighlightBackground": "#498FAE26", + "editor.findRangeHighlightBackground": "#232627", + "editor.hoverHighlightBackground": "#232627", + "editor.lineHighlightBackground": "#232627", + "editor.rangeHighlightBackground": "#232627", + "editorLink.activeForeground": "#4a8fad", + "editorWhitespace.foreground": "#8888884D", + "editorIndentGuide.background": "#8184854D", + "editorIndentGuide.activeBackground": "#818485", + "editorRuler.foreground": "#848484", + "editorCodeLens.foreground": "#888888", + "editorBracketMatch.background": "#498FAE55", + "editorBracketMatch.border": "#2A2B2CFF", + "editorWidget.background": "#1F2223", + "editorWidget.border": "#2A2B2CFF", + "editorWidget.foreground": "#bfbfbf", + "editorSuggestWidget.background": "#1F2223", + "editorSuggestWidget.border": "#2A2B2CFF", + "editorSuggestWidget.foreground": "#bfbfbf", + "editorSuggestWidget.highlightForeground": "#bfbfbf", + "editorSuggestWidget.selectedBackground": "#498FAE26", + "editorHoverWidget.background": "#1F2223", + "editorHoverWidget.border": "#2A2B2CFF", + "peekView.border": "#2A2B2CFF", + "peekViewEditor.background": "#191B1D", + "peekViewEditor.matchHighlightBackground": "#498FAE33", + "peekViewResult.background": "#232627", + "peekViewResult.fileForeground": "#bfbfbf", + "peekViewResult.lineForeground": "#888888", + "peekViewResult.matchHighlightBackground": "#498FAE33", + "peekViewResult.selectionBackground": "#498FAE26", + "peekViewResult.selectionForeground": "#bfbfbf", + "peekViewTitle.background": "#232627", + "peekViewTitleDescription.foreground": "#888888", + "peekViewTitleLabel.foreground": "#bfbfbf", + "editorGutter.background": "#121416", + "editorGutter.addedBackground": "#71C792", + "editorGutter.deletedBackground": "#EF8773", + "diffEditor.insertedTextBackground": "#71C79233", + "diffEditor.removedTextBackground": "#EF877333", + "editorOverviewRuler.border": "#2A2B2CFF", + "editorOverviewRuler.findMatchForeground": "#4a8fad99", + "editorOverviewRuler.modifiedForeground": "#6ab890", + "editorOverviewRuler.addedForeground": "#73c991", + "editorOverviewRuler.deletedForeground": "#f48771", + "editorOverviewRuler.errorForeground": "#f48771", + "editorOverviewRuler.warningForeground": "#e5ba7d", + "panel.background": "#191B1D", + "panel.border": "#2A2B2CFF", + "panelTitle.activeBorder": "#498FAD", + "panelTitle.activeForeground": "#bfbfbf", + "panelTitle.inactiveForeground": "#888888", + "statusBar.background": "#191B1D", + "statusBar.foreground": "#bfbfbf", + "statusBar.border": "#2A2B2CFF", + "statusBar.focusBorder": "#498FADB3", + "statusBar.debuggingBackground": "#498FAE", + "statusBar.debuggingForeground": "#FFFFFF", + "statusBar.noFolderBackground": "#191B1D", + "statusBar.noFolderForeground": "#bfbfbf", + "statusBarItem.activeBackground": "#498FAE", + "statusBarItem.hoverBackground": "#252829", + "statusBarItem.focusBorder": "#498FADB3", + "statusBarItem.prominentBackground": "#498FAE", + "statusBarItem.prominentForeground": "#FFFFFF", + "statusBarItem.prominentHoverBackground": "#498FAE", + "tab.activeBackground": "#121416", + "tab.activeForeground": "#bfbfbf", + "tab.inactiveBackground": "#191B1D", + "tab.inactiveForeground": "#888888", + "tab.border": "#2A2B2CFF", + "tab.lastPinnedBorder": "#2A2B2CFF", + "tab.activeBorder": "#121314", + "tab.hoverBackground": "#252829", + "tab.hoverForeground": "#bfbfbf", + "tab.unfocusedActiveBackground": "#121416", + "tab.unfocusedActiveForeground": "#888888", + "tab.unfocusedInactiveBackground": "#191B1D", + "tab.unfocusedInactiveForeground": "#444444", + "editorGroupHeader.tabsBackground": "#191B1D", + "editorGroupHeader.tabsBorder": "#2A2B2CFF", + "breadcrumb.foreground": "#888888", + "breadcrumb.background": "#121416", + "breadcrumb.focusForeground": "#bfbfbf", + "breadcrumb.activeSelectionForeground": "#bfbfbf", + "breadcrumbPicker.background": "#1F2223", + "notificationCenter.border": "#2A2B2CFF", + "notificationCenterHeader.foreground": "#bfbfbf", + "notificationCenterHeader.background": "#232627", + "notificationToast.border": "#2A2B2CFF", + "notifications.foreground": "#bfbfbf", + "notifications.background": "#1F2223", + "notifications.border": "#2A2B2CFF", + "notificationLink.foreground": "#4a8fad", + "extensionButton.prominentBackground": "#498FAE", + "extensionButton.prominentForeground": "#FFFFFF", + "extensionButton.prominentHoverBackground": "#4D94B4", + "pickerGroup.border": "#2A2B2CFF", + "pickerGroup.foreground": "#bfbfbf", + "quickInput.background": "#1F2223", + "quickInput.foreground": "#bfbfbf", + "quickInputList.focusBackground": "#498FAE26", + "quickInputList.focusForeground": "#bfbfbf", + "quickInputList.focusIconForeground": "#bfbfbf", + "quickInputList.hoverBackground": "#505354", + "terminal.selectionBackground": "#498FAE33", + "terminalCursor.foreground": "#bfbfbf", + "terminalCursor.background": "#191B1D", + "gitDecoration.addedResourceForeground": "#73c991", + "gitDecoration.modifiedResourceForeground": "#e5ba7d", + "gitDecoration.deletedResourceForeground": "#f48771", + "gitDecoration.untrackedResourceForeground": "#73c991", + "gitDecoration.ignoredResourceForeground": "#8C8C8C", + "gitDecoration.conflictingResourceForeground": "#f48771", + "gitDecoration.stageModifiedResourceForeground": "#e5ba7d", + "gitDecoration.stageDeletedResourceForeground": "#f48771", + "quickInputTitle.background": "#1F2223", + "quickInput.border": "#333536", + "chat.requestBubbleBackground": "#498FAE26", + "chat.requestBubbleHoverBackground": "#498FAE46" + }, + "tokenColors": [ + { + "scope": [ + "comment" + ], + "settings": { + "foreground": "#6A9955" + } + }, + { + "scope": [ + "keyword", + "storage.modifier", + "storage.type" + ], + "settings": { + "foreground": "#569cd6" + } + }, + { + "scope": [ + "string" + ], + "settings": { + "foreground": "#ce9178" + } + }, + { + "name": "Function declarations", + "scope": [ + "entity.name.function", + "support.function", + "support.constant.handlebars" + ], + "settings": { + "foreground": "#DCDCAA" + } + }, + { + "name": "Types declaration and references", + "scope": [ + "support.class", + "support.type", + "entity.name.type", + "entity.name.namespace", + "entity.name.class" + ], + "settings": { + "foreground": "#4EC9B0" + } + }, + { + "name": "Control flow keywords", + "scope": [ + "keyword.control" + ], + "settings": { + "foreground": "#C586C0" + } + }, + { + "name": "Variable and parameter name", + "scope": [ + "variable", + "meta.definition.variable.name", + "support.variable", + "entity.name.variable" + ], + "settings": { + "foreground": "#9CDCFE" + } + }, + { + "name": "Constants and enums", + "scope": [ + "variable.other.constant", + "variable.other.enummember" + ], + "settings": { + "foreground": "#4FC1FF" + } + }, + { + "name": "Numbers", + "scope": [ + "constant.numeric" + ], + "settings": { + "foreground": "#b5cea8" + } + } + ], + "semanticHighlighting": true, + "semanticTokenColors": { + "newOperator": "#C586C0", + "stringLiteral": "#ce9178", + "customLiteral": "#DCDCAA", + "numberLiteral": "#b5cea8" + } +} \ No newline at end of file diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json new file mode 100644 index 0000000000000..2ef9666b6565f --- /dev/null +++ b/extensions/theme-2026/themes/2026-light.json @@ -0,0 +1,334 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "2026 Light", + "type": "light", + "colors": { + "foreground": "#202020", + "disabledForeground": "#BBBBBB", + "errorForeground": "#ad0707", + "descriptionForeground": "#666666", + "icon.foreground": "#666666", + "focusBorder": "#4466CCFF", + "textBlockQuote.background": "#E9E9E9", + "textBlockQuote.border": "#EEEEEE00", + "textCodeBlock.background": "#E9E9E9", + "textLink.foreground": "#3457C0", + "textLink.activeForeground": "#395DC9", + "textPreformat.foreground": "#666666", + "textSeparator.foreground": "#EEEEEE00", + "button.background": "#4466CC", + "button.foreground": "#FFFFFF", + "button.hoverBackground": "#4F6FCF", + "button.border": "#EEEEEE00", + "button.secondaryBackground": "#E9E9E9", + "button.secondaryForeground": "#202020", + "button.secondaryHoverBackground": "#F5F5F5", + "checkbox.background": "#E9E9E9", + "checkbox.border": "#EEEEEE00", + "checkbox.foreground": "#202020", + "dropdown.background": "#F9F9F9", + "dropdown.border": "#D6D7D8", + "dropdown.foreground": "#202020", + "dropdown.listBackground": "#FCFCFC", + "input.background": "#F9F9F9", + "input.border": "#D6D7D880", + "input.foreground": "#202020", + "input.placeholderForeground": "#999999", + "inputOption.activeBackground": "#4466CC33", + "inputOption.activeForeground": "#202020", + "inputOption.activeBorder": "#EEEEEE00", + "inputValidation.errorBackground": "#F9F9F9", + "inputValidation.errorBorder": "#EEEEEE00", + "inputValidation.errorForeground": "#202020", + "inputValidation.infoBackground": "#F9F9F9", + "inputValidation.infoBorder": "#EEEEEE00", + "inputValidation.infoForeground": "#202020", + "inputValidation.warningBackground": "#F9F9F9", + "inputValidation.warningBorder": "#EEEEEE00", + "inputValidation.warningForeground": "#202020", + "scrollbar.shadow": "#F5F6F84D", + "scrollbarSlider.background": "#4466CC33", + "scrollbarSlider.hoverBackground": "#4466CC4D", + "scrollbarSlider.activeBackground": "#4466CC4D", + "badge.background": "#4466CC", + "badge.foreground": "#FFFFFF", + "progressBar.background": "#666666", + "list.activeSelectionBackground": "#4466CC26", + "list.activeSelectionForeground": "#202020", + "list.inactiveSelectionBackground": "#E9E9E9", + "list.inactiveSelectionForeground": "#202020", + "list.hoverBackground": "#FFFFFF", + "list.hoverForeground": "#202020", + "list.dropBackground": "#4466CC1A", + "list.focusBackground": "#4466CC26", + "list.focusForeground": "#202020", + "list.focusOutline": "#4466CCFF", + "list.highlightForeground": "#202020", + "list.invalidItemForeground": "#BBBBBB", + "list.errorForeground": "#ad0707", + "list.warningForeground": "#667309", + "activityBar.background": "#F9F9F9", + "activityBar.foreground": "#202020", + "activityBar.inactiveForeground": "#666666", + "activityBar.border": "#EEEEEE00", + "activityBar.activeBorder": "#EEEEEE00", + "activityBar.activeFocusBorder": "#4466CCFF", + "activityBarBadge.background": "#4466CC", + "activityBarBadge.foreground": "#FFFFFF", + "sideBar.background": "#F9F9F9", + "sideBar.foreground": "#202020", + "sideBar.border": "#EEEEEE00", + "sideBarTitle.foreground": "#202020", + "sideBarSectionHeader.background": "#F9F9F9", + "sideBarSectionHeader.foreground": "#202020", + "sideBarSectionHeader.border": "#EEEEEE00", + "titleBar.activeBackground": "#F9F9F9", + "titleBar.activeForeground": "#424242", + "titleBar.inactiveBackground": "#F9F9F9", + "titleBar.inactiveForeground": "#666666", + "titleBar.border": "#EEEEEE00", + "menubar.selectionBackground": "#E9E9E9", + "menubar.selectionForeground": "#202020", + "menu.background": "#FCFCFC", + "menu.foreground": "#202020", + "menu.selectionBackground": "#4466CC26", + "menu.selectionForeground": "#202020", + "menu.separatorBackground": "#F4F4F4", + "menu.border": "#EEEEEE00", + "commandCenter.foreground": "#202020", + "commandCenter.activeForeground": "#202020", + "commandCenter.background": "#F9F9F9", + "commandCenter.activeBackground": "#FFFFFF", + "commandCenter.border": "#D6D7D880", + "editor.background": "#FDFDFD", + "editor.foreground": "#202123", + "editorLineNumber.foreground": "#656668", + "editorLineNumber.activeForeground": "#202123", + "editorCursor.foreground": "#202123", + "editor.selectionBackground": "#4466CC26", + "editor.inactiveSelectionBackground": "#4466CC80", + "editor.selectionHighlightBackground": "#4466CC1A", + "editor.wordHighlightBackground": "#4466CC33", + "editor.wordHighlightStrongBackground": "#4466CC33", + "editor.findMatchBackground": "#4466CC4D", + "editor.findMatchHighlightBackground": "#4466CC26", + "editor.findRangeHighlightBackground": "#E9E9E9", + "editor.hoverHighlightBackground": "#E9E9E9", + "editor.lineHighlightBackground": "#E9E9E9", + "editor.rangeHighlightBackground": "#E9E9E9", + "editorLink.activeForeground": "#4466CC", + "editorWhitespace.foreground": "#6666664D", + "editorIndentGuide.background": "#F4F4F44D", + "editorIndentGuide.activeBackground": "#F4F4F4", + "editorRuler.foreground": "#F4F4F4", + "editorCodeLens.foreground": "#666666", + "editorBracketMatch.background": "#4466CC55", + "editorBracketMatch.border": "#EEEEEE00", + "editorWidget.background": "#FCFCFC", + "editorWidget.border": "#EEEEEE00", + "editorWidget.foreground": "#202020", + "editorSuggestWidget.background": "#FCFCFC", + "editorSuggestWidget.border": "#EEEEEE00", + "editorSuggestWidget.foreground": "#202020", + "editorSuggestWidget.highlightForeground": "#202020", + "editorSuggestWidget.selectedBackground": "#4466CC26", + "editorHoverWidget.background": "#FCFCFC", + "editorHoverWidget.border": "#EEEEEE00", + "peekView.border": "#EEEEEE00", + "peekViewEditor.background": "#F9F9F9", + "peekViewEditor.matchHighlightBackground": "#4466CC33", + "peekViewResult.background": "#E9E9E9", + "peekViewResult.fileForeground": "#202020", + "peekViewResult.lineForeground": "#666666", + "peekViewResult.matchHighlightBackground": "#4466CC33", + "peekViewResult.selectionBackground": "#4466CC26", + "peekViewResult.selectionForeground": "#202020", + "peekViewTitle.background": "#E9E9E9", + "peekViewTitleDescription.foreground": "#666666", + "peekViewTitleLabel.foreground": "#202020", + "editorGutter.background": "#FDFDFD", + "editorGutter.addedBackground": "#587c0c", + "editorGutter.deletedBackground": "#ad0707", + "diffEditor.insertedTextBackground": "#587c0c26", + "diffEditor.removedTextBackground": "#ad070726", + "editorOverviewRuler.border": "#EEEEEE00", + "editorOverviewRuler.findMatchForeground": "#4466CC99", + "editorOverviewRuler.modifiedForeground": "#007acc", + "editorOverviewRuler.addedForeground": "#587c0c", + "editorOverviewRuler.deletedForeground": "#ad0707", + "editorOverviewRuler.errorForeground": "#ad0707", + "editorOverviewRuler.warningForeground": "#667309", + "panel.background": "#F9F9F9", + "panel.border": "#EEEEEE00", + "panelTitle.activeBorder": "#4466CC", + "panelTitle.activeForeground": "#202020", + "panelTitle.inactiveForeground": "#666666", + "statusBar.background": "#F9F9F9", + "statusBar.foreground": "#202020", + "statusBar.border": "#EEEEEE00", + "statusBar.focusBorder": "#4466CCFF", + "statusBar.debuggingBackground": "#4466CC", + "statusBar.debuggingForeground": "#FFFFFF", + "statusBar.noFolderBackground": "#F9F9F9", + "statusBar.noFolderForeground": "#202020", + "statusBarItem.activeBackground": "#4466CC", + "statusBarItem.hoverBackground": "#FFFFFF", + "statusBarItem.focusBorder": "#4466CCFF", + "statusBarItem.prominentBackground": "#4466CC", + "statusBarItem.prominentForeground": "#FFFFFF", + "statusBarItem.prominentHoverBackground": "#4466CC", + "tab.activeBackground": "#FDFDFD", + "tab.activeForeground": "#202020", + "tab.inactiveBackground": "#F9F9F9", + "tab.inactiveForeground": "#666666", + "tab.border": "#EEEEEE00", + "tab.lastPinnedBorder": "#EEEEEE00", + "tab.activeBorder": "#FBFBFD", + "tab.hoverBackground": "#FFFFFF", + "tab.hoverForeground": "#202020", + "tab.unfocusedActiveBackground": "#FDFDFD", + "tab.unfocusedActiveForeground": "#666666", + "tab.unfocusedInactiveBackground": "#F9F9F9", + "tab.unfocusedInactiveForeground": "#BBBBBB", + "editorGroupHeader.tabsBackground": "#F9F9F9", + "editorGroupHeader.tabsBorder": "#EEEEEE00", + "breadcrumb.foreground": "#666666", + "breadcrumb.background": "#FDFDFD", + "breadcrumb.focusForeground": "#202020", + "breadcrumb.activeSelectionForeground": "#202020", + "breadcrumbPicker.background": "#FCFCFC", + "notificationCenter.border": "#EEEEEE00", + "notificationCenterHeader.foreground": "#202020", + "notificationCenterHeader.background": "#E9E9E9", + "notificationToast.border": "#EEEEEE00", + "notifications.foreground": "#202020", + "notifications.background": "#FCFCFC", + "notifications.border": "#EEEEEE00", + "notificationLink.foreground": "#4466CC", + "extensionButton.prominentBackground": "#4466CC", + "extensionButton.prominentForeground": "#FFFFFF", + "extensionButton.prominentHoverBackground": "#4F6FCF", + "pickerGroup.border": "#EEEEEE00", + "pickerGroup.foreground": "#202020", + "quickInput.background": "#FCFCFC", + "quickInput.foreground": "#202020", + "quickInputList.focusBackground": "#4466CC26", + "quickInputList.focusForeground": "#202020", + "quickInputList.focusIconForeground": "#202020", + "quickInputList.hoverBackground": "#FAFAFA", + "terminal.selectionBackground": "#4466CC33", + "terminalCursor.foreground": "#202020", + "terminalCursor.background": "#F9F9F9", + "gitDecoration.addedResourceForeground": "#587c0c", + "gitDecoration.modifiedResourceForeground": "#667309", + "gitDecoration.deletedResourceForeground": "#ad0707", + "gitDecoration.untrackedResourceForeground": "#587c0c", + "gitDecoration.ignoredResourceForeground": "#8E8E90", + "gitDecoration.conflictingResourceForeground": "#ad0707", + "gitDecoration.stageModifiedResourceForeground": "#667309", + "gitDecoration.stageDeletedResourceForeground": "#ad0707", + "quickInputTitle.background": "#FCFCFC", + "quickInput.border": "#D6D7D8", + "chat.requestBubbleBackground": "#4466CC1A", + "chat.requestBubbleHoverBackground": "#4466CC26" + }, + "tokenColors": [ + { + "scope": [ + "comment" + ], + "settings": { + "foreground": "#008000" + } + }, + { + "scope": [ + "keyword", + "storage.modifier", + "storage.type" + ], + "settings": { + "foreground": "#0000ff" + } + }, + { + "scope": [ + "string" + ], + "settings": { + "foreground": "#a31515" + } + }, + { + "name": "Function declarations", + "scope": [ + "entity.name.function", + "support.function", + "support.constant.handlebars" + ], + "settings": { + "foreground": "#795E26" + } + }, + { + "name": "Types declaration and references", + "scope": [ + "support.class", + "support.type", + "entity.name.type", + "entity.name.namespace", + "entity.name.class" + ], + "settings": { + "foreground": "#267f99" + } + }, + { + "name": "Control flow keywords", + "scope": [ + "keyword.control" + ], + "settings": { + "foreground": "#AF00DB" + } + }, + { + "name": "Variable and parameter name", + "scope": [ + "variable", + "meta.definition.variable.name", + "support.variable", + "entity.name.variable" + ], + "settings": { + "foreground": "#001080" + } + }, + { + "name": "Constants and enums", + "scope": [ + "variable.other.constant", + "variable.other.enummember" + ], + "settings": { + "foreground": "#0070C1" + } + }, + { + "name": "Numbers", + "scope": [ + "constant.numeric" + ], + "settings": { + "foreground": "#098658" + } + } + ], + "semanticHighlighting": true, + "semanticTokenColors": { + "newOperator": "#AF00DB", + "stringLiteral": "#a31515", + "customLiteral": "#795E26", + "numberLiteral": "#098658" + } +} \ No newline at end of file diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css new file mode 100644 index 0000000000000..589033dccf3ae --- /dev/null +++ b/extensions/theme-2026/themes/styles.css @@ -0,0 +1,234 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Stealth Shadows - shadow-based depth for UI elements, controlled by workbench.stealthShadows.enabled */ + +/* Activity Bar */ +.monaco-workbench .part.activitybar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 50; position: relative; } +.monaco-workbench.activitybar-right .part.activitybar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); } + +/* Sidebar */ +.monaco-workbench .part.sidebar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 40; position: relative; } +.monaco-workbench.sidebar-right .part.sidebar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); } +.monaco-workbench .part.auxiliarybar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 40; position: relative; } + +/* Panel */ +.monaco-workbench .part.panel { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 35; position: relative; } +.monaco-workbench.panel-position-left .part.panel { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); } +.monaco-workbench.panel-position-right .part.panel { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); } + +/* Editor */ +.monaco-workbench .part.editor { position: relative; } +.monaco-workbench .part.editor > .content .editor-group-container > .title { box-shadow: none; position: relative; z-index: 10; } +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { box-shadow: 0 0 5px rgba(0, 0, 0, 0.10); position: relative; z-index: 5; border-radius: 4px 4px 0 0; border-top: none !important; } +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover:not(.active) { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); } + +/* Title Bar */ +.monaco-workbench .part.titlebar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 60; position: relative; overflow: visible !important; } +.monaco-workbench .part.titlebar .titlebar-container, +.monaco-workbench .part.titlebar .titlebar-center, +.monaco-workbench .part.titlebar .titlebar-center .window-title, +.monaco-workbench .part.titlebar .command-center, +.monaco-workbench .part.titlebar .command-center .monaco-action-bar, +.monaco-workbench .part.titlebar .command-center .actions-container { overflow: visible !important; } + +/* Status Bar */ +.monaco-workbench .part.statusbar { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08); z-index: 55; position: relative; } + +/* Quick Input (Command Palette) */ +.monaco-workbench .quick-input-widget { box-shadow: 0 0 20px rgba(0, 0, 0, 0.15) !important; border-radius: 12px; overflow: hidden; background: rgba(252, 252, 253, 0.5) !important; backdrop-filter: blur(20px) saturate(100%); -webkit-backdrop-filter: blur(40px) saturate(180%); } +.monaco-workbench.vs-dark .quick-input-widget, +.monaco-workbench.hc-black .quick-input-widget { background: rgba(10, 10, 11, 0.5) !important; backdrop-filter: blur(40px) saturate(180%); -webkit-backdrop-filter: blur(40px) saturate(180%); box-shadow: 0 0 20px rgba(0, 0, 0, 0.15) !important; } +.monaco-workbench .quick-input-widget .monaco-list-rows { background-color: transparent !important; } +/* .monaco-workbench .quick-input-widget, */ +/* .monaco-workbench .quick-input-widget *, */ +.monaco-workbench .quick-input-widget .quick-input-header, +.monaco-workbench .quick-input-widget .quick-input-list, +.monaco-workbench .quick-input-widget .quick-input-titlebar, +.monaco-workbench .quick-input-widget .quick-input-title, +.monaco-workbench .quick-input-widget .quick-input-description, +.monaco-workbench .quick-input-widget .quick-input-filter, +.monaco-workbench .quick-input-widget .quick-input-action, +.monaco-workbench .quick-input-widget .quick-input-message, +/* .monaco-workbench .quick-input-widget .monaco-inputbox, */ +.monaco-workbench .quick-input-widget .monaco-list, +.monaco-workbench .quick-input-widget .monaco-list-row { border: none !important; border-color: transparent !important; outline: none !important; } +.monaco-workbench .quick-input-widget .monaco-inputbox { box-shadow: none !important; background: transparent !important; } +.monaco-workbench .quick-input-widget .quick-input-filter .monaco-inputbox { background: rgba(249, 250, 251, 0.4) !important; border-radius: 6px; } +.monaco-workbench.vs-dark .quick-input-widget .quick-input-filter .monaco-inputbox, +.monaco-workbench.hc-black .quick-input-widget .quick-input-filter .monaco-inputbox { background: rgba(20, 20, 22, 0.6) !important; } + +.monaco-workbench .quick-input-widget .monaco-list.list_id_6:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: color-mix(in srgb, var(--vscode-list-hoverBackground) 40%, transparent); } + + +/* Chat Widget */ +.monaco-workbench .interactive-session .chat-input-container { box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.08); border-radius: 6px; } +.monaco-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor, +.monaco-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor .monaco-editor-background { background-color: var(--vscode-panel-background, var(--vscode-sideBar-background)) !important; } +.monaco-workbench .interactive-session .chat-editing-session .chat-editing-session-container { border-radius: 4px 4px 0 0; } +.monaco-workbench .interactive-input-part:has(.chat-editing-session > .chat-editing-session-container) .chat-input-container { border-radius: 0 0 6px 6px; } +.monaco-workbench .part.panel .interactive-session, +.monaco-workbench .part.auxiliarybar .interactive-session { position: relative; } + +.monaco-workbench .interactive-session .chat-editing-session .chat-editing-session-container { + background-color: transparent !important; +} + +/* Notifications */ +.monaco-workbench .notifications-toasts { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); } +.monaco-workbench .notification-toast { box-shadow: none !important; border: none !important; } +.monaco-workbench .notifications-center { border: none !important; } + +/* Context Menus */ +.monaco-workbench .monaco-menu .monaco-action-bar.vertical { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 12px; overflow: hidden; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); } +.monaco-workbench .context-view .monaco-menu { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 12px; } +.monaco-workbench.vs-dark .monaco-menu .monaco-action-bar.vertical, +.monaco-workbench.vs-dark .context-view .monaco-menu, +.monaco-workbench.hc-black .monaco-menu .monaco-action-bar.vertical, +.monaco-workbench.hc-black .context-view .monaco-menu { background: rgba(10, 10, 11, 0.85) !important; } +.monaco-workbench.vs .monaco-menu .monaco-action-bar.vertical, +.monaco-workbench.vs .context-view .monaco-menu { background: rgba(252, 252, 253, 0.85) !important; } + +.monaco-workbench .action-widget { background: rgba(252, 252, 253, 0.5) !important; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);} +.monaco-workbench.vs-dark .action-widget { background: rgba(10, 10, 11, 0.5) !important; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);} +.monaco-workbench .action-widget .action-widget-action-bar {background: transparent;} + +/* Suggest Widget */ +.monaco-workbench .monaco-editor .suggest-widget { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 12px; overflow: hidden; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); } +.monaco-workbench.vs-dark .monaco-editor .suggest-widget, +.monaco-workbench.hc-black .monaco-editor .suggest-widget { background: rgba(10, 10, 11, 0.85) !important; } +.monaco-workbench.vs .monaco-editor .suggest-widget { background: rgba(252, 252, 253, 0.85) !important; } + +/* Find Widget */ +.monaco-workbench .monaco-editor .find-widget { box-shadow: 0 0 8px rgba(0, 0, 0, 0.12); border: none; border-radius: 8px; } + +/* Dialog */ +.monaco-workbench .monaco-dialog-box { box-shadow: 0 0 20px rgba(0, 0, 0, 0.18); border: none; border-radius: 12px; overflow: hidden; } + +/* Peek View */ +.monaco-workbench .monaco-editor .peekview-widget { box-shadow: 0 0 8px rgba(0, 0, 0, 0.12); border: none; background: var(--vscode-editor-background, #EDEDED) !important; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-radius: 8px; overflow: hidden; } +.monaco-workbench .monaco-editor .peekview-widget .head, +.monaco-workbench .monaco-editor .peekview-widget .body { background: transparent !important; } +.monaco-workbench .monaco-editor .peekview-widget .ref-tree { background: var(--vscode-editor-background, #EDEDED) !important; } + +/* Settings */ +.monaco-workbench .settings-editor .settings-toc-container { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); } + +/* Welcome Tiles */ +.monaco-workbench .part.editor .welcomePageContainer .tile { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10); border: none; border-radius: 8px; } +.monaco-workbench .part.editor .welcomePageContainer .tile:hover { box-shadow: 0 0 8px rgba(0, 0, 0, 0.12); } + +/* Extensions */ +.monaco-workbench .extensions-list .extension-list-item { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border: none; border-radius: 6px; margin: 4px 0; } +.monaco-workbench .extensions-list .extension-list-item:hover { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10); } + +/* Breadcrumbs */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .breadcrumbs-control { box-shadow: 0 0 6px rgba(0, 0, 0, 0.08) !important; background: rgba(252, 252, 253, 0.65) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; } +.monaco-workbench.vs-dark .part.editor > .content .editor-group-container > .title .breadcrumbs-control, +.monaco-workbench.hc-black .part.editor > .content .editor-group-container > .title .breadcrumbs-control { background: rgba(10, 10, 11, 0.85) !important; box-shadow: 0 0 6px rgba(0, 0, 0, 0.08) !important; } + +/* Input Boxes */ +.monaco-workbench .monaco-inputbox, +.monaco-workbench .suggest-input-container { box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.08); border: none; } + +.monaco-inputbox .monaco-action-bar .action-item .codicon, +.monaco-workbench .search-container .input-box, +.monaco-custom-toggle { + color: var(--vscode-icon-foreground) !important; +} + +/* .scm-view .scm-editor { + box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.08); +} */ + +/* Buttons */ +.monaco-workbench .monaco-button { box-shadow: 0 0 2px rgba(0, 0, 0, 0.06); } +.monaco-workbench .monaco-button:hover { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); } +.monaco-workbench .monaco-button:active { box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); } + +/* Dropdowns */ +.monaco-workbench .monaco-dropdown .dropdown-menu { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 8px; } + +/* Terminal */ +.monaco-workbench .pane-body.integrated-terminal { box-shadow: inset 0 0 4px rgba(255, 255, 255, 0.1); } + +/* SCM */ +.monaco-workbench .scm-view .scm-provider { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border-radius: 6px; } + +/* Debug Toolbar */ +.monaco-workbench .debug-toolbar { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 8px; } + +/* Action Widget */ +.monaco-workbench .action-widget { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14) !important; border: none !important; border-radius: 8px; } + +/* Parameter Hints */ +.monaco-workbench .monaco-editor .parameter-hints-widget { box-shadow: 0 0 8px rgba(0, 0, 0, 0.12); border: none; border-radius: 12px; overflow: hidden; backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); } +.monaco-workbench.vs-dark .monaco-editor .parameter-hints-widget, +.monaco-workbench.hc-black .monaco-editor .parameter-hints-widget { background: rgba(10, 10, 11, 0.85) !important; } +.monaco-workbench.vs .monaco-editor .parameter-hints-widget { background: rgba(252, 252, 253, 0.85) !important; } + +/* Minimap */ +.monaco-workbench .monaco-editor .minimap { background: rgba(252, 252, 253, 0.65) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; border-radius: 8px 0 0 8px; } +.monaco-workbench .monaco-editor .minimap canvas { opacity: 0.85; } +.monaco-workbench.vs-dark .monaco-editor .minimap, +.monaco-workbench.hc-black .monaco-editor .minimap { background: rgba(5, 5, 6, 0.85) !important; box-shadow: 0 0 6px rgba(0, 0, 0, 0.10) !important; } +.monaco-workbench .monaco-editor .minimap-shadow-visible { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10); } + +/* Sticky Scroll */ +.monaco-workbench .monaco-editor .sticky-widget { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10) !important; border-bottom: none !important; border: none !important; background: rgba(252, 252, 253, 0.65) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; border-radius: 0 0 8px 8px !important; } +.monaco-workbench .monaco-editor .sticky-widget *, +.monaco-workbench .monaco-editor .sticky-widget > *, +.monaco-workbench .monaco-editor .sticky-widget .sticky-widget-line-numbers, +.monaco-workbench .monaco-editor .sticky-widget .sticky-widget-lines-scrollable, +.monaco-workbench .monaco-editor .sticky-widget .sticky-widget-lines, +.monaco-workbench .monaco-editor .sticky-widget .sticky-line-number, +.monaco-workbench .monaco-editor .sticky-widget .sticky-line-content, +.monaco-workbench .monaco-editor .sticky-widget .sticky-line-content:hover { background-color: transparent !important; background: transparent !important; } +.monaco-workbench.vs-dark .monaco-editor .sticky-widget, +.monaco-workbench.hc-black .monaco-editor .sticky-widget { background: rgba(10, 10, 11, 0.85) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; box-shadow: 0 0 6px rgba(0, 0, 0, 0.10) !important; } +.monaco-workbench .monaco-editor .sticky-widget-focus-preview, +.monaco-workbench .monaco-editor .sticky-scroll-focus-line, +.monaco-workbench .monaco-editor .focused .sticky-widget, +.monaco-workbench .monaco-editor:has(.sticky-widget:focus-within) .sticky-widget { background: rgba(252, 252, 253, 0.75) !important; backdrop-filter: blur(40px) saturate(180%) !important; -webkit-backdrop-filter: blur(40px) saturate(180%) !important; box-shadow: 0 0 8px rgba(0, 0, 0, 0.12) !important; border-radius: 0 0 8px 8px !important; } +.monaco-workbench.vs-dark .monaco-editor .sticky-widget-focus-preview, +.monaco-workbench.vs-dark .monaco-editor .sticky-scroll-focus-line, +.monaco-workbench.vs-dark .monaco-editor .focused .sticky-widget, +.monaco-workbench.vs-dark .monaco-editor:has(.sticky-widget:focus-within) .sticky-widget, +.monaco-workbench.hc-black .monaco-editor .sticky-widget-focus-preview, +.monaco-workbench.hc-black .monaco-editor .sticky-scroll-focus-line, +.monaco-workbench.hc-black .monaco-editor .focused .sticky-widget, +.monaco-workbench.hc-black .monaco-editor:has(.sticky-widget:focus-within) .sticky-widget { background: rgba(10, 10, 11, 0.90) !important; box-shadow: 0 0 8px rgba(0, 0, 0, 0.12) !important; } + +/* Notebook */ +.monaco-workbench .notebookOverlay .monaco-list .cell-focus-indicator { box-shadow: 0 0 4px rgba(0, 0, 0, 0.08); border-radius: 6px; } +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator { box-shadow: 0 0 6px rgba(0, 0, 0, 0.10); } + +/* Inline Chat */ +.monaco-workbench .monaco-editor .inline-chat { box-shadow: 0 0 12px rgba(0, 0, 0, 0.14); border: none; border-radius: 12px; } + +/* Command Center */ +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center { box-shadow: 0 0 8px rgba(0, 0, 0, 0.09) !important; border-radius: 8px !important; background: rgba(249, 250, 251, 0.55) !important; backdrop-filter: blur(24px) saturate(150%); -webkit-backdrop-filter: blur(24px) saturate(150%); overflow: visible !important; } +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:hover { box-shadow: 0 0 10px rgba(0, 0, 0, 0.11) !important; background: rgba(249, 250, 251, 0.70) !important; } +.monaco-workbench.vs-dark .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center, +.monaco-workbench.hc-black .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center { background: rgba(10, 10, 11, 0.75) !important; backdrop-filter: blur(24px) saturate(150%); -webkit-backdrop-filter: blur(24px) saturate(150%); } +.monaco-workbench.vs-dark .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:hover, +.monaco-workbench.hc-black .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:hover { background: rgba(15, 15, 17, 0.85) !important; } +.monaco-workbench .part.titlebar .command-center .agent-status-pill { + box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.08); +} +.monaco-workbench .part.titlebar .command-center .agent-status-pill:hover { + box-shadow: none; + background-color: transparent; +} + +/* Remove Borders */ +.monaco-workbench.vs .part.sidebar { border-right: none !important; border-left: none !important; } +.monaco-workbench.vs .part.auxiliarybar { border-right: none !important; border-left: none !important; } +.monaco-workbench .part.panel { border-top: none !important; } +.monaco-workbench.vs .part.activitybar { border-right: none !important; border-left: none !important; } +.monaco-workbench.vs .part.titlebar { border-bottom: none !important; } +.monaco-workbench.vs .part.statusbar { border-top: none !important; } +.monaco-workbench .part.editor > .content .editor-group-container { border: none !important; } +.monaco-workbench .pane-composite-part:not(.empty) > .header { border-bottom: none !important; } diff --git a/package-lock.json b/package-lock.json index b3bc0bec88cfa..d6d45b9c4a887 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@anthropic-ai/sandbox-runtime": "0.0.23", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.4", @@ -178,6 +179,35 @@ "node": ">=6.0.0" } }, + "node_modules/@anthropic-ai/sandbox-runtime": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.23.tgz", + "integrity": "sha512-Np0VRH6D71cGoJZvd8hCz1LMfwg9ERJovrOJSCz5aSQSQJPWPNIFPV1wfc8oAhJpStOuYkot+EmXOkRRxuGMCQ==", + "license": "Apache-2.0", + "dependencies": { + "@pondwader/socks5-server": "^1.0.10", + "@types/lodash-es": "^4.17.12", + "commander": "^12.1.0", + "lodash-es": "^4.17.21", + "shell-quote": "^1.8.3", + "zod": "^3.24.1" + }, + "bin": { + "srt": "dist/cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@anthropic-ai/sandbox-runtime/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@azure-rest/ai-translation-text": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/@azure-rest/ai-translation-text/-/ai-translation-text-1.0.0-beta.1.tgz", @@ -2032,6 +2062,12 @@ "node": ">=18" } }, + "node_modules/@pondwader/socks5-server": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@pondwader/socks5-server/-/socks5-server-1.0.10.tgz", + "integrity": "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg==", + "license": "MIT" + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -2328,6 +2364,21 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -11775,6 +11826,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash-es": { + "version": "4.17.22", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz", + "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==", + "license": "MIT" + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -15339,10 +15396,16 @@ } }, "node_modules/shell-quote": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/sigmund": { "version": "1.0.1", @@ -18279,6 +18342,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zx": { "version": "8.8.5", "resolved": "https://registry.npmjs.org/zx/-/zx-8.8.5.tgz", diff --git a/package.json b/package.json index a6112b973122c..b3198745e54c2 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "update-build-ts-version": "npm install -D typescript@next && npm install -D @typescript/native-preview && (cd build && npm run typecheck)" }, "dependencies": { + "@anthropic-ai/sandbox-runtime": "0.0.23", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "^2.5.4", diff --git a/src/main.ts b/src/main.ts index fc2d71affbd2a..ecbbb16547916 100644 --- a/src/main.ts +++ b/src/main.ts @@ -227,10 +227,7 @@ function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { // bypass any specified proxy for the given semi-colon-separated list of hosts 'proxy-bypass-list', - 'remote-debugging-port', - - // Enable recovery from invalid Graphite recordings - 'enable-graphite-invalid-recording-recovery' + 'remote-debugging-port' ]; if (process.platform === 'linux') { @@ -359,6 +356,10 @@ function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { // use up to 2 app.commandLine.appendSwitch('max-active-webgl-contexts', '32'); + // Disable Skia Graphite backend. + // Refs https://github.com/microsoft/vscode/issues/284162 + app.commandLine.appendSwitch('disable-skia-graphite'); + return argvConfig; } @@ -377,7 +378,6 @@ interface IArgvConfig { readonly 'use-inmemory-secretstorage'?: boolean; readonly 'enable-rdp-display-tracking'?: boolean; readonly 'remote-debugging-port'?: string; - readonly 'enable-graphite-invalid-recording-recovery'?: boolean; } function readArgvConfigSync(): IArgvConfig { diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 101b46af347c2..8e5564d466e1e 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -768,6 +768,10 @@ export class ViewModel extends Disposable implements IViewModel { * Gives a hint that a lot of requests are about to come in for these line numbers. */ public setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void { + if (this._lines.getViewLineCount() === 0) { + // No visible lines to set viewport on + return; + } this._viewportStart.update(this, startLineNumber); } diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index a6e0856069e3e..b511c59081e5f 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -9,12 +9,10 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { VSBuffer } from '../../../base/common/buffer.js'; import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions } from '../common/browserView.js'; import { EVENT_KEY_CODE_MAP, KeyCode, KeyMod, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js'; -import { IThemeMainService } from '../../theme/electron-main/themeMainService.js'; import { IWindowsMainService } from '../../windows/electron-main/windows.js'; import { IBaseWindow, ICodeWindow } from '../../window/electron-main/window.js'; import { IAuxiliaryWindowsMainService } from '../../auxiliaryWindow/electron-main/auxiliaryWindows.js'; import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryWindow.js'; -import { ILogService } from '../../log/common/log.js'; import { isMacintosh } from '../../../base/common/platform.js'; /** Key combinations that are used in system-level shortcuts. */ @@ -74,10 +72,8 @@ export class BrowserView extends Disposable { constructor( viewSession: Electron.Session, private readonly storageScope: BrowserViewStorageScope, - @IThemeMainService private readonly themeMainService: IThemeMainService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService, - @ILogService private readonly logService: ILogService + @IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService ) { super(); @@ -111,9 +107,6 @@ export class BrowserView extends Disposable { }); this.setupEventListeners(); - - // Create and register plugins for this web contents - this._register(new ThemePlugin(this._view, this.themeMainService, this.logService)); } private setupEventListeners(): void { @@ -519,59 +512,3 @@ export class BrowserView extends Disposable { return this.auxiliaryWindowsMainService.getWindowByWebContents(contents); } } - -export class ThemePlugin extends Disposable { - private readonly _webContents: Electron.WebContents; - private _injectedCSSKey?: string; - - constructor( - private readonly _view: Electron.WebContentsView, - private readonly themeMainService: IThemeMainService, - private readonly logService: ILogService - ) { - super(); - this._webContents = _view.webContents; - - // Set view background to match editor background - this.applyBackgroundColor(); - - // Apply theme when page loads - this._webContents.on('did-finish-load', () => this.applyTheme()); - - // Update theme when VS Code theme changes - this._register(this.themeMainService.onDidChangeColorScheme(() => { - this.applyBackgroundColor(); - this.applyTheme(); - })); - } - - private applyBackgroundColor(): void { - const backgroundColor = this.themeMainService.getBackgroundColor(); - this._view.setBackgroundColor(backgroundColor); - } - - private async applyTheme(): Promise { - if (this._webContents.isDestroyed()) { - return; - } - - const colorScheme = this.themeMainService.getColorScheme().dark ? 'dark' : 'light'; - - try { - // Remove previous theme CSS if it exists - if (this._injectedCSSKey) { - await this._webContents.removeInsertedCSS(this._injectedCSSKey); - } - - // Insert new theme CSS - this._injectedCSSKey = await this._webContents.insertCSS(` - /* VS Code theme override */ - :root { - color-scheme: ${colorScheme}; - } - `); - } catch (error) { - this.logService.error('ThemePlugin: Failed to inject CSS', error); - } - } -} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 120c1acb4bca2..38849ed2a4aec 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1558,6 +1558,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatPromptFiles'); return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.prompt, provider); }, + registerSkillProvider(provider: vscode.SkillProvider): vscode.Disposable { + checkProposedApiEnabled(extension, 'chatPromptFiles'); + return extHostChatAgents2.registerPromptFileProvider(extension, PromptsType.skill, provider); + }, }; // namespace: lm @@ -1963,6 +1967,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CustomAgentChatResource: extHostTypes.CustomAgentChatResource, InstructionsChatResource: extHostTypes.InstructionsChatResource, PromptFileChatResource: extHostTypes.PromptFileChatResource, + SkillChatResource: extHostTypes.SkillChatResource, }; }; } diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 7a724abf1c2ac..a9285d5534d11 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -417,7 +417,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS private readonly _relatedFilesProviders = new Map(); private static _contributionsProviderIdPool = 0; - private readonly _promptFileProviders = new Map(); + private readonly _promptFileProviders = new Map(); private readonly _sessionDisposables: DisposableResourceMap = this._register(new DisposableResourceMap()); private readonly _completionDisposables: DisposableMap = this._register(new DisposableMap()); @@ -499,9 +499,9 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS /** * Internal method that handles all prompt file provider types. - * Routes custom agents, instructions, and prompt files to the unified internal implementation. + * Routes custom agents, instructions, prompt files, and skills to the unified internal implementation. */ - registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: vscode.CustomAgentProvider | vscode.InstructionsProvider | vscode.PromptFileProvider): vscode.Disposable { + registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: vscode.CustomAgentProvider | vscode.InstructionsProvider | vscode.PromptFileProvider | vscode.SkillProvider): vscode.Disposable { const handle = ExtHostChatAgents2._contributionsProviderIdPool++; this._promptFileProviders.set(handle, { extension, provider }); this._proxy.$registerPromptFileProvider(handle, type, extension.identifier); @@ -521,6 +521,9 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS case PromptsType.prompt: changeEvent = (provider as vscode.PromptFileProvider).onDidChangePromptFiles; break; + case PromptsType.skill: + changeEvent = (provider as vscode.SkillProvider).onDidChangeSkills; + break; } if (changeEvent) { @@ -554,7 +557,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } const provider = providerData.provider; - let resources: vscode.CustomAgentChatResource[] | vscode.InstructionsChatResource[] | vscode.PromptFileChatResource[] | undefined; + let resources: vscode.CustomAgentChatResource[] | vscode.InstructionsChatResource[] | vscode.PromptFileChatResource[] | vscode.SkillChatResource[] | undefined; switch (type) { case PromptsType.agent: resources = await (provider as vscode.CustomAgentProvider).provideCustomAgents(context, token) ?? undefined; @@ -566,7 +569,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS resources = await (provider as vscode.PromptFileProvider).providePromptFiles(context, token) ?? undefined; break; case PromptsType.skill: - throw new Error('Skills prompt file provider not implemented yet'); + resources = await (provider as vscode.SkillProvider).provideSkills(context, token) ?? undefined; + break; } // Convert ChatResourceDescriptor to IPromptFileResource format @@ -583,7 +587,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS }); } - convertChatResourceDescriptorToPromptFileResource(resource: vscode.ChatResourceDescriptor, extensionId: string): IPromptFileResource { + convertChatResourceDescriptorToPromptFileResource(resource: vscode.ChatResourceDescriptor | vscode.ChatResourceUriDescriptor, extensionId: string): IPromptFileResource { if (URI.isUri(resource)) { // Plain URI return { uri: resource }; diff --git a/src/vs/workbench/api/common/extHostChatContext.ts b/src/vs/workbench/api/common/extHostChatContext.ts index 761fca70dbb71..74710e0309e4a 100644 --- a/src/vs/workbench/api/common/extHostChatContext.ts +++ b/src/vs/workbench/api/common/extHostChatContext.ts @@ -167,7 +167,7 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext if (!provider.onDidChangeWorkspaceChatContext || !provider.provideWorkspaceChatContext) { return; } - disposables.add(provider.onDidChangeWorkspaceChatContext(async () => { + const provideWorkspaceContext = async () => { const workspaceContexts = await provider.provideWorkspaceChatContext!(CancellationToken.None); const resolvedContexts: IChatContextItem[] = []; for (const item of workspaceContexts ?? []) { @@ -183,9 +183,12 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext const resolved = await this._doResolve(provider, contextItem, item, CancellationToken.None); resolvedContexts.push(resolved); } + return this._proxy.$updateWorkspaceContextItems(handle, resolvedContexts); + }; - this._proxy.$updateWorkspaceContextItems(handle, resolvedContexts); - })); + disposables.add(provider.onDidChangeWorkspaceChatContext(async () => provideWorkspaceContext())); + // kick off initial workspace context fetch + provideWorkspaceContext(); } private _getProvider(handle: number): vscode.ChatContextProvider { diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 4311937f5c284..826c9ef464454 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1406,6 +1406,7 @@ class InlineCompletionAdapter { requestUuid: context.requestUuid, requestIssuedDateTime: context.requestIssuedDateTime, earliestShownDateTime: context.earliestShownDateTime, + changeHint: context.changeHint, }, token); if (!result) { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 03dc0c2207562..b24085ac81331 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3902,4 +3902,9 @@ export class InstructionsChatResource implements vscode.InstructionsChatResource export class PromptFileChatResource implements vscode.PromptFileChatResource { constructor(public readonly resource: vscode.ChatResourceDescriptor) { } } + +@es5ClassCompat +export class SkillChatResource implements vscode.SkillChatResource { + constructor(public readonly resource: vscode.ChatResourceUriDescriptor) { } +} //#endregion diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 88bc828b318b2..05a7a11324351 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -10,7 +10,7 @@ import { format, compare, splitLines } from '../../../../base/common/strings.js' import { extname, basename, isEqual } from '../../../../base/common/resources.js'; import { areFunctions, assertReturnsDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; -import { Action } from '../../../../base/common/actions.js'; +import { IAction, toAction } from '../../../../base/common/actions.js'; import { Language } from '../../../../base/common/platform.js'; import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js'; import { IFileEditorInput, EditorResourceAccessor, IEditorPane, SideBySideEditor } from '../../../common/editor.js'; @@ -1107,25 +1107,6 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { } } -export class ShowLanguageExtensionsAction extends Action { - - static readonly ID = 'workbench.action.showLanguageExtensions'; - - constructor( - private fileExtension: string, - @ICommandService private readonly commandService: ICommandService, - @IExtensionGalleryService galleryService: IExtensionGalleryService - ) { - super(ShowLanguageExtensionsAction.ID, localize('showLanguageExtensions', "Search Marketplace Extensions for '{0}'...", fileExtension)); - - this.enabled = galleryService.isEnabled(); - } - - override async run(): Promise { - await this.commandService.executeCommand('workbench.extensions.action.showExtensionsForLanguage', this.fileExtension); - } -} - export class ChangeLanguageAction extends Action2 { static readonly ID = 'workbench.action.editor.changeLanguageMode'; @@ -1159,9 +1140,10 @@ export class ChangeLanguageAction extends Action2 { const languageDetectionService = accessor.get(ILanguageDetectionService); const textFileService = accessor.get(ITextFileService); const preferencesService = accessor.get(IPreferencesService); - const instantiationService = accessor.get(IInstantiationService); const configurationService = accessor.get(IConfigurationService); const telemetryService = accessor.get(ITelemetryService); + const commandService = accessor.get(ICommandService); + const galleryService = accessor.get(IExtensionGalleryService); const activeTextEditorControl = getCodeEditor(editorService.activeTextEditorControl); if (!activeTextEditorControl) { @@ -1211,12 +1193,16 @@ export class ChangeLanguageAction extends Action2 { // Offer action to configure via settings let configureLanguageAssociations: IQuickPickItem | undefined; let configureLanguageSettings: IQuickPickItem | undefined; - let galleryAction: Action | undefined; + let galleryAction: IAction | undefined; if (hasLanguageSupport && resource) { const ext = extname(resource) || basename(resource); - galleryAction = instantiationService.createInstance(ShowLanguageExtensionsAction, ext); - if (galleryAction.enabled) { + if (galleryService.isEnabled()) { + galleryAction = toAction({ + id: 'workbench.action.showLanguageExtensions', + label: localize('showLanguageExtensions', "Search Marketplace Extensions for '{0}'...", ext), + run: () => commandService.executeCommand('workbench.extensions.action.showExtensionsForLanguage', ext) + }); picks.unshift(galleryAction); } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 376f1dda25e23..428c63041da5f 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -22,7 +22,7 @@ import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/b import { IPosition, Position } from '../../../../editor/common/core/position.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { IModelService } from '../../../../editor/common/services/model.js'; -import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js'; + import { AccessibilityHelpNLS } from '../../../../editor/common/standaloneStrings.js'; import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js'; import { FloatingEditorToolbar } from '../../../../editor/contrib/floatingMenu/browser/floatingMenu.js'; @@ -66,7 +66,7 @@ interface ICodeBlock { chatSessionResource: URI | undefined; } -export class AccessibleView extends Disposable implements ITextModelContentProvider { +export class AccessibleView extends Disposable { private _editorWidget: CodeEditorWidget; private _accessiblityHelpIsShown: IContextKey; @@ -111,7 +111,6 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi @ICommandService private readonly _commandService: ICommandService, @IChatCodeBlockContextProviderService private readonly _codeBlockContextProviderService: IChatCodeBlockContextProviderService, @IStorageService private readonly _storageService: IStorageService, - @ITextModelService private readonly textModelResolverService: ITextModelService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, ) { @@ -167,7 +166,6 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi readOnly: true, fontFamily: 'var(--monaco-monospace-font)' }; - this.textModelResolverService.registerTextModelContentProvider(Schemas.accessibleView, this); this._editorWidget = this._register(this._instantiationService.createInstance(CodeEditorWidget, this._container, editorOptions, codeEditorWidgetOptions)); this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => { @@ -216,10 +214,6 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi } } - provideTextContent(resource: URI): Promise | null { - return this._getTextModel(resource); - } - private _resetContextKeys(): void { this._accessiblityHelpIsShown.reset(); this._accessibleViewIsShown.reset(); @@ -545,6 +539,10 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi this._accessibleViewGoToSymbolSupported.set(this._goToSymbolsSupported() ? this.getSymbols()?.length! > 0 : false); } + private _getStableUri(providerId: string): URI { + return URI.from({ path: `accessible-view-${providerId}`, scheme: Schemas.accessibleView }); + } + private _updateContent(provider: AccesibleViewContentProvider, updatedContent?: string): void { let content = updatedContent ?? provider.provideContent(); if (provider.options.type === AccessibleViewType.View) { @@ -590,11 +588,20 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi this.calculateCodeBlocks(this._currentContent); this._updateContextKeys(provider, true); const widgetIsFocused = this._editorWidget.hasTextFocus() || this._editorWidget.hasWidgetFocus(); - this._getTextModel(URI.from({ path: `accessible-view-${provider.id}`, scheme: Schemas.accessibleView, fragment: this._currentContent })).then((model) => { + const stableUri = this._getStableUri(provider.id); + this._getTextModel(stableUri).then((model) => { if (!model) { return; } - this._editorWidget.setModel(model); + // Update the content of the existing model instead of creating a new one + // This preserves the cursor position when content changes + const currentContent = this._currentContent ?? ''; + if (model.getValue() !== currentContent) { + model.setValue(currentContent); + } + if (this._editorWidget.getModel() !== model) { + this._editorWidget.setModel(model); + } const domNode = this._editorWidget.getDomNode(); if (!domNode) { return; @@ -720,7 +727,8 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi if (existing && !existing.isDisposed()) { return existing; } - return this._modelService.createModel(resource.fragment, null, resource, false); + // Create an empty model - content will be set via setValue() to preserve cursor position + return this._modelService.createModel('', null, resource, false); } private _goToSymbolsSupported(): boolean { diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts index 4cb1a09d8c57c..d7cd83cd4bf33 100644 --- a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts @@ -5,9 +5,10 @@ import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; -import { isMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { IMarkdownString, isMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; import { stripIcons } from '../../../../../base/common/iconLabels.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../../platform/accessibility/browser/accessibleView.js'; import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; @@ -15,10 +16,11 @@ import { ServicesAccessor } from '../../../../../platform/instantiation/common/i import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { migrateLegacyTerminalToolSpecificData } from '../../common/chat.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; -import { IChatToolInvocation } from '../../common/chatService/chatService.js'; +import { IChatExtensionsContent, IChatPullRequestContent, IChatSubagentToolInvocationData, IChatTerminalToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, ILegacyChatTerminalToolInvocationData, IToolResultOutputDetailsSerialized, isLegacyChatTerminalToolInvocationData } from '../../common/chatService/chatService.js'; import { isResponseVM } from '../../common/model/chatViewModel.js'; -import { isToolResultInputOutputDetails, isToolResultOutputDetails, toolContentToA11yString } from '../../common/tools/languageModelToolsService.js'; +import { IToolResultInputOutputDetails, IToolResultOutputDetails, isToolResultInputOutputDetails, isToolResultOutputDetails, toolContentToA11yString } from '../../common/tools/languageModelToolsService.js'; import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js'; +import { Location } from '../../../../../editor/common/languages.js'; export class ChatResponseAccessibleView implements IAccessibleViewImplementation { readonly priority = 100; @@ -46,6 +48,137 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplementation } } +type ToolSpecificData = IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData; +type ResultDetails = Array | IToolResultInputOutputDetails | IToolResultOutputDetails | IToolResultOutputDetailsSerialized; + +function isOutputDetailsSerialized(obj: unknown): obj is IToolResultOutputDetailsSerialized { + return typeof obj === 'object' && obj !== null && 'output' in obj && + typeof (obj as IToolResultOutputDetailsSerialized).output === 'object' && + (obj as IToolResultOutputDetailsSerialized).output?.type === 'data' && + typeof (obj as IToolResultOutputDetailsSerialized).output?.base64Data === 'string'; +} + +export function getToolSpecificDataDescription(toolSpecificData: ToolSpecificData | undefined): string { + if (!toolSpecificData) { + return ''; + } + + if (isLegacyChatTerminalToolInvocationData(toolSpecificData) || toolSpecificData.kind === 'terminal') { + const terminalData = migrateLegacyTerminalToolSpecificData(toolSpecificData); + return terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original; + } + + switch (toolSpecificData.kind) { + case 'subagent': { + const parts: string[] = []; + if (toolSpecificData.agentName) { + parts.push(localize('subagentName', "Agent: {0}", toolSpecificData.agentName)); + } + if (toolSpecificData.description) { + parts.push(toolSpecificData.description); + } + if (toolSpecificData.prompt) { + parts.push(localize('subagentPrompt', "Task: {0}", toolSpecificData.prompt)); + } + return parts.join('. ') || ''; + } + case 'extensions': + return toolSpecificData.extensions.length > 0 + ? localize('extensionsList', "Extensions: {0}", toolSpecificData.extensions.join(', ')) + : ''; + case 'todoList': { + const todos = toolSpecificData.todoList; + if (todos.length === 0) { + return ''; + } + const todoDescriptions = todos.map(t => + localize('todoItem', "{0} ({1}): {2}", t.title, t.status, t.description) + ); + return localize('todoListCount', "{0} items: {1}", todos.length, todoDescriptions.join('; ')); + } + case 'pullRequest': + return localize('pullRequestInfo', "PR: {0} by {1}", toolSpecificData.title, toolSpecificData.author); + case 'input': + return typeof toolSpecificData.rawInput === 'string' + ? toolSpecificData.rawInput + : JSON.stringify(toolSpecificData.rawInput); + default: + return ''; + } +} + +export function getResultDetailsDescription(resultDetails: ResultDetails | undefined): { input?: string; files?: string[]; isError?: boolean } { + if (!resultDetails) { + return {}; + } + + if (Array.isArray(resultDetails)) { + const files = resultDetails.map(ref => { + if (URI.isUri(ref)) { + return ref.fsPath || ref.path; + } + return ref.uri.fsPath || ref.uri.path; + }); + return { files }; + } + + if (isToolResultInputOutputDetails(resultDetails)) { + return { + input: resultDetails.input, + isError: resultDetails.isError + }; + } + + if (isOutputDetailsSerialized(resultDetails)) { + return { + input: localize('binaryOutput', "{0} data", resultDetails.output.mimeType) + }; + } + + if (isToolResultOutputDetails(resultDetails)) { + return { + input: localize('binaryOutput', "{0} data", resultDetails.output.mimeType) + }; + } + + return {}; +} + +export function getToolInvocationA11yDescription( + invocationMessage: string | undefined, + pastTenseMessage: string | undefined, + toolSpecificData: ToolSpecificData | undefined, + resultDetails: ResultDetails | undefined, + isComplete: boolean +): string { + const parts: string[] = []; + + const message = isComplete && pastTenseMessage ? pastTenseMessage : invocationMessage; + if (message) { + parts.push(message); + } + + const toolDataDesc = getToolSpecificDataDescription(toolSpecificData); + if (toolDataDesc) { + parts.push(toolDataDesc); + } + + if (isComplete && resultDetails) { + const details = getResultDetailsDescription(resultDetails); + if (details.isError) { + parts.unshift(localize('errored', "Errored")); + } + if (details.input && !toolDataDesc) { + parts.push(localize('input', "Input: {0}", details.input)); + } + if (details.files && details.files.length > 0) { + parts.push(localize('files', "Files: {0}", details.files.join(', '))); + } + } + + return parts.join('. '); +} + class ChatResponseAccessibleProvider extends Disposable implements IAccessibleViewContentProvider { private _focusedItem!: ChatTreeItem; private readonly _focusedItemDisposables = this._register(new DisposableStore()); @@ -76,6 +209,10 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi } } + private _renderMessageAsPlaintext(message: string | IMarkdownString): string { + return typeof message === 'string' ? message : stripIcons(renderAsPlaintext(message, { useLinkFormatter: true })); + } + private _getContent(item: ChatTreeItem): string { let responseContent = isResponseVM(item) ? item.response.toString() : ''; if (!responseContent && 'errorDetails' in item && item.errorDetails) { @@ -100,30 +237,17 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi for (const toolInvocation of toolInvocations) { const state = toolInvocation.state.get(); if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation && state.confirmationMessages?.title) { - const title = typeof state.confirmationMessages.title === 'string' ? state.confirmationMessages.title : state.confirmationMessages.title.value; - const message = typeof state.confirmationMessages.message === 'string' ? state.confirmationMessages.message : stripIcons(renderAsPlaintext(state.confirmationMessages.message!)); - let input = ''; - if (toolInvocation.toolSpecificData) { - if (toolInvocation.toolSpecificData?.kind === 'terminal') { - const terminalData = migrateLegacyTerminalToolSpecificData(toolInvocation.toolSpecificData); - input = terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original; - } else if (toolInvocation.toolSpecificData?.kind === 'subagent') { - input = toolInvocation.toolSpecificData.description ?? ''; - } else { - input = toolInvocation.toolSpecificData?.kind === 'extensions' - ? JSON.stringify(toolInvocation.toolSpecificData.extensions) - : toolInvocation.toolSpecificData?.kind === 'todoList' - ? JSON.stringify(toolInvocation.toolSpecificData.todoList) - : toolInvocation.toolSpecificData?.kind === 'pullRequest' - ? JSON.stringify(toolInvocation.toolSpecificData) - : JSON.stringify(toolInvocation.toolSpecificData.rawInput); - } - } + const title = this._renderMessageAsPlaintext(state.confirmationMessages.title); + const message = state.confirmationMessages.message ? this._renderMessageAsPlaintext(state.confirmationMessages.message) : ''; + const toolDataDesc = getToolSpecificDataDescription(toolInvocation.toolSpecificData); responseContent += `${title}`; - if (input) { - responseContent += `: ${input}`; + if (toolDataDesc) { + responseContent += `: ${toolDataDesc}`; } - responseContent += `\n${message}\n`; + if (message) { + responseContent += `\n${message}`; + } + responseContent += '\n'; } else if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { const postApprovalDetails = isToolResultInputOutputDetails(state.resultDetails) ? state.resultDetails.input @@ -133,23 +257,35 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi responseContent += localize('toolPostApprovalA11yView', "Approve results of {0}? Result: ", toolInvocation.toolId) + (postApprovalDetails ?? '') + '\n'; } else { const resultDetails = IChatToolInvocation.resultDetails(toolInvocation); - if (resultDetails && 'input' in resultDetails) { - responseContent += '\n' + (resultDetails.isError ? 'Errored ' : 'Completed '); - responseContent += `${`${typeof toolInvocation.invocationMessage === 'string' ? toolInvocation.invocationMessage : stripIcons(renderAsPlaintext(toolInvocation.invocationMessage))} with input: ${resultDetails.input}`}\n`; + const isComplete = IChatToolInvocation.isComplete(toolInvocation); + const description = getToolInvocationA11yDescription( + this._renderMessageAsPlaintext(toolInvocation.invocationMessage), + toolInvocation.pastTenseMessage ? this._renderMessageAsPlaintext(toolInvocation.pastTenseMessage) : undefined, + toolInvocation.toolSpecificData, + resultDetails, + isComplete + ); + if (description) { + responseContent += '\n' + description + '\n'; } } } const pastConfirmations = item.response.value.filter(item => item.kind === 'toolInvocationSerialized'); for (const pastConfirmation of pastConfirmations) { - if (pastConfirmation.isComplete && pastConfirmation.resultDetails && 'input' in pastConfirmation.resultDetails) { - if (pastConfirmation.pastTenseMessage) { - responseContent += `\n${`${typeof pastConfirmation.pastTenseMessage === 'string' ? pastConfirmation.pastTenseMessage : stripIcons(renderAsPlaintext(pastConfirmation.pastTenseMessage))} with input: ${pastConfirmation.resultDetails.input}`}\n`; - } + const description = getToolInvocationA11yDescription( + this._renderMessageAsPlaintext(pastConfirmation.invocationMessage), + pastConfirmation.pastTenseMessage ? this._renderMessageAsPlaintext(pastConfirmation.pastTenseMessage) : undefined, + pastConfirmation.toolSpecificData, + pastConfirmation.resultDetails, + pastConfirmation.isComplete + ); + if (description) { + responseContent += '\n' + description + '\n'; } } } - const plainText = renderAsPlaintext(new MarkdownString(responseContent), { includeCodeBlocksFences: true }); + const plainText = renderAsPlaintext(new MarkdownString(responseContent), { includeCodeBlocksFences: true, useLinkFormatter: true }); return this._normalizeWhitespace(plainText); } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 17c8d9f3a5aae..c7a9442143b77 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -13,7 +13,7 @@ import { ICompressedTreeNode } from '../../../../../base/browser/ui/tree/compres import { ICompressibleKeyboardNavigationLabelProvider, ICompressibleTreeRenderer } from '../../../../../base/browser/ui/tree/objectTree.js'; import { ITreeNode, ITreeElementRenderDetails, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction } from '../../../../../base/browser/ui/tree/tree.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { AgentSessionSection, AgentSessionStatus, getAgentChangesSummary, hasValidDiff, IAgentSession, IAgentSessionSection, IAgentSessionsModel, isAgentSession, isAgentSessionSection, isAgentSessionsModel, isLocalAgentSessionItem, isSessionInProgressStatus } from './agentSessionsModel.js'; +import { AgentSessionSection, AgentSessionStatus, getAgentChangesSummary, hasValidDiff, IAgentSession, IAgentSessionSection, IAgentSessionsModel, isAgentSession, isAgentSessionSection, isAgentSessionsModel, isSessionInProgressStatus } from './agentSessionsModel.js'; import { IconLabel } from '../../../../../base/browser/ui/iconLabel/iconLabel.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { Codicon } from '../../../../../base/common/codicons.js'; @@ -329,12 +329,8 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer this._getResolvedCommand() })); + // Use presentationOverrides for display if available (e.g., extracted Python code with syntax highlighting) + const displayCommand = terminalData.presentationOverrides?.commandLine ?? command; + const displayLanguage = terminalData.presentationOverrides?.language ?? terminalData.language; const titlePart = this._register(_instantiationService.createInstance( ChatQueryTitlePart, elements.commandBlock, new MarkdownString([ - `\`\`\`${terminalData.language}`, - `${command.replaceAll('```', '\\`\\`\\`')}`, + `\`\`\`${displayLanguage}`, + `${displayCommand.replaceAll('```', '\\`\\`\\`')}`, `\`\`\`` ].join('\n'), { supportThemeIcons: true }), undefined, @@ -1003,7 +1006,7 @@ class ChatTerminalToolOutputSection extends Disposable { return true; } await liveTerminalInstance.xtermReadyPromise; - if (liveTerminalInstance.isDisposed || !liveTerminalInstance.xterm) { + if (this._store.isDisposed || liveTerminalInstance.isDisposed || !liveTerminalInstance.xterm) { this._disposeLiveMirror(); return false; } @@ -1037,6 +1040,9 @@ class ChatTerminalToolOutputSection extends Disposable { this._layoutOutput(snapshot.lineCount ?? 0, this._lastRenderedMaxColumnWidth); return; } + if (this._store.isDisposed) { + return; + } dom.clearNode(this._terminalContainer); this._snapshotMirror = this._register(this._instantiationService.createInstance(DetachedTerminalSnapshotMirror, snapshot, this._getStoredTheme)); await this._snapshotMirror.attach(this._terminalContainer); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 4a665c1c09592..a1ef74cebd99e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -1315,6 +1315,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer confirmation transition to finalize thinking + if (toolInvocation.kind === 'toolInvocation' && IChatToolInvocation.isStreaming(toolInvocation)) { + let wasStreaming = true; + part.addDisposable(autorun(reader => { + const state = toolInvocation.state.read(reader); + if (wasStreaming && state.type !== IChatToolInvocation.StateKind.Streaming) { + wasStreaming = false; + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { + if (part.domNode) { + const wrapper = part.domNode.parentElement; + if (wrapper?.classList.contains('chat-thinking-tool-wrapper')) { + wrapper.remove(); + } + templateData.value.appendChild(part.domNode); + } + this.finalizeCurrentThinkingPart(context, templateData); + } + } + })); + } } } else { this.finalizeCurrentThinkingPart(context, templateData); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts index f0576287c7a7b..a6ed9a671a428 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts @@ -184,6 +184,12 @@ export class ModelPickerActionItem extends ChatInputPickerActionViewItem { return statusIcon && tooltip ? `${label} • ${tooltip}` : label; } + protected override setAriaLabelAttributes(element: HTMLElement): void { + super.setAriaLabelAttributes(element); + const modelName = this.currentModel?.metadata.name ?? localize('chat.modelPicker.auto', "Auto"); + element.ariaLabel = localize('chat.modelPicker.ariaLabel', "Pick Model, {0}", modelName); + } + protected override renderLabel(element: HTMLElement): IDisposable | null { const { name, statusIcon } = this.currentModel?.metadata || {}; const domChildren = []; diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 82e01d19a974e..10963bf00ea55 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -6,6 +6,7 @@ import './media/chatViewPane.css'; import { $, addDisposableListener, append, EventHelper, EventType, getWindow, setVisibility } from '../../../../../../base/browser/dom.js'; import { StandardMouseEvent } from '../../../../../../base/browser/mouseEvent.js'; +import { Button } from '../../../../../../base/browser/ui/button/button.js'; import { Orientation, Sash } from '../../../../../../base/browser/ui/sash/sash.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Event } from '../../../../../../base/common/event.js'; @@ -16,6 +17,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { localize } from '../../../../../../nls.js'; import { MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; import { MenuId } from '../../../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; @@ -27,6 +29,7 @@ import { ILogService } from '../../../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { defaultButtonStyles } from '../../../../../../platform/theme/browser/defaultStyles.js'; import { editorBackground } from '../../../../../../platform/theme/common/colorRegistry.js'; import { ChatViewTitleControl } from './chatViewTitleControl.js'; import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; @@ -46,6 +49,7 @@ import { LocalChatSessionUri, getChatSessionType } from '../../../common/model/c import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../../common/constants.js'; import { AgentSessionsControl } from '../../agentSessions/agentSessionsControl.js'; import { AgentSessionsListDelegate } from '../../agentSessions/agentSessionsViewer.js'; +import { ACTION_ID_NEW_CHAT } from '../../actions/chatActions.js'; import { ChatWidget } from '../../widget/chatWidget.js'; import { ChatViewWelcomeController, IViewWelcomeDelegate } from '../../viewsWelcome/chatViewWelcomeController.js'; import { IWorkbenchLayoutService, LayoutSettings, Position } from '../../../../../services/layout/browser/layoutService.js'; @@ -108,6 +112,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { @IProgressService private readonly progressService: IProgressService, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, + @ICommandService private readonly commandService: ICommandService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -317,6 +322,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private sessionsContainer: HTMLElement | undefined; private sessionsTitleContainer: HTMLElement | undefined; private sessionsTitle: HTMLElement | undefined; + private sessionsNewButtonContainer: HTMLElement | undefined; private sessionsControlContainer: HTMLElement | undefined; private sessionsControl: AgentSessionsControl | undefined; private sessionsCount = 0; @@ -379,6 +385,12 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { sessionsToolbarContainer.classList.toggle('filtered', !sessionsFilter.isDefault()); })); + // New Session Button + const newSessionButtonContainer = this.sessionsNewButtonContainer = append(sessionsContainer, $('.agent-sessions-new-button-container')); + const newSessionButton = this._register(new Button(newSessionButtonContainer, { ...defaultButtonStyles, secondary: true })); + newSessionButton.label = localize('newSession', "New Session"); + this._register(newSessionButton.onDidClick(() => this.commandService.executeCommand(ACTION_ID_NEW_CHAT))); + // Sessions Control this.sessionsControlContainer = append(sessionsContainer, $('.agent-sessions-control-container')); const sessionsControl = this.sessionsControl = this._register(this.instantiationService.createInstance(AgentSessionsControl, this.sessionsControlContainer, { @@ -500,6 +512,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { // Sessions control: stacked if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { newSessionsContainerVisible = + !!this.chatEntitlementService.sentiment.installed && // chat is installed (otherwise make room for terms and welcome) (!this._widget || this._widget?.isEmpty()) && // chat widget empty !this.welcomeController?.isShowingWelcome.get() && // welcome not showing (this.sessionsCount > 0 || !this.sessionsViewerLimited); // has sessions or is showing all sessions @@ -924,6 +937,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { let availableSessionsHeight = height - this.sessionsTitleContainer.offsetHeight; if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { availableSessionsHeight -= Math.max(ChatViewPane.MIN_CHAT_WIDGET_HEIGHT, this._widget?.input?.contentHeight ?? 0); + } else { + availableSessionsHeight -= this.sessionsNewButtonContainer?.offsetHeight ?? 0; } // Show as sidebar diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts index 56f1721323f4e..dac6fd6bf45ab 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts @@ -4,13 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import './media/chatViewTitleControl.css'; -import { addDisposableListener, EventType, h, hide, show } from '../../../../../../base/browser/dom.js'; +import { addDisposableListener, EventType, h } from '../../../../../../base/browser/dom.js'; import { renderAsPlaintext } from '../../../../../../base/browser/markdownRenderer.js'; import { Gesture, EventType as TouchEventType } from '../../../../../../base/browser/touch.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { Disposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { MarshalledId } from '../../../../../../base/common/marshallingIds.js'; import { localize } from '../../../../../../nls.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; @@ -20,7 +19,6 @@ import { IInstantiationService, ServicesAccessor } from '../../../../../../platf import { IChatViewTitleActionContext } from '../../../common/actions/chatActions.js'; import { IChatModel } from '../../../common/model/chatModel.js'; import { ChatConfiguration } from '../../../common/constants.js'; -import { AgentSessionProviders, getAgentSessionProviderIcon } from '../../agentSessions/agentSessions.js'; import { ActionViewItem, IActionViewItemOptions } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction } from '../../../../../../base/common/actions.js'; import { AgentSessionsPicker } from '../../agentSessions/agentSessionsPicker.js'; @@ -101,7 +99,6 @@ export class ChatViewTitleControl extends Disposable { private render(parent: HTMLElement): void { const elements = h('div.chat-view-title-container', [ h('div.chat-view-title-navigation-toolbar@navigationToolbar'), - h('span.chat-view-title-icon@icon'), h('div.chat-view-title-actions-toolbar@actionsToolbar'), ]); @@ -110,7 +107,7 @@ export class ChatViewTitleControl extends Disposable { actionViewItemProvider: (action: IAction) => { if (action.id === ChatViewTitleControl.PICK_AGENT_SESSION_ACTION_ID) { this.titleLabel.value = new ChatViewTitleLabel(action); - this.titleLabel.value.updateTitle(this.title ?? ChatViewTitleControl.DEFAULT_TITLE, this.getIcon()); + this.titleLabel.value.updateTitle(this.title ?? ChatViewTitleControl.DEFAULT_TITLE); return this.titleLabel.value; } @@ -177,7 +174,7 @@ export class ChatViewTitleControl extends Disposable { } this.titleContainer.classList.toggle('visible', this.shouldRender()); - this.titleLabel.value?.updateTitle(title, this.getIcon()); + this.titleLabel.value?.updateTitle(title); const currentHeight = this.getHeight(); if (currentHeight !== this.lastKnownHeight) { @@ -187,17 +184,6 @@ export class ChatViewTitleControl extends Disposable { } } - private getIcon(): ThemeIcon | undefined { - const sessionType = this.model?.contributedChatSession?.chatSessionType; - switch (sessionType) { - case AgentSessionProviders.Background: - case AgentSessionProviders.Cloud: - return getAgentSessionProviderIcon(sessionType); - } - - return undefined; - } - private shouldRender(): boolean { if (!this.isEnabled()) { return false; // title hidden via setting @@ -222,10 +208,8 @@ export class ChatViewTitleControl extends Disposable { class ChatViewTitleLabel extends ActionViewItem { private title: string | undefined; - private icon: ThemeIcon | undefined; private titleLabel: HTMLSpanElement | undefined = undefined; - private titleIcon: HTMLSpanElement | undefined = undefined; constructor(action: IAction, options?: IActionViewItemOptions) { super(null, action, { ...options, icon: false, label: true }); @@ -237,19 +221,15 @@ class ChatViewTitleLabel extends ActionViewItem { container.classList.add('chat-view-title-action-item'); this.label?.classList.add('chat-view-title-label-container'); - this.titleIcon = this.label?.appendChild(h('span').root); this.titleLabel = this.label?.appendChild(h('span.chat-view-title-label').root); this.updateLabel(); - this.updateIcon(); } - updateTitle(title: string, icon: ThemeIcon | undefined): void { + updateTitle(title: string): void { this.title = title; - this.icon = icon; this.updateLabel(); - this.updateIcon(); } protected override updateLabel(): void { @@ -263,18 +243,4 @@ class ChatViewTitleLabel extends ActionViewItem { this.titleLabel.textContent = ''; } } - - private updateIcon(): void { - if (!this.titleIcon) { - return; - } - - if (this.icon) { - this.titleIcon.className = ThemeIcon.asClassName(this.icon); - show(this.titleIcon); - } else { - this.titleIcon.className = ''; - hide(this.titleIcon); - } - } } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css index b4eadde9ef239..8f48fa4e03b91 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css @@ -85,6 +85,11 @@ .agent-sessions-container { border-bottom: 1px solid var(--vscode-panel-border); } + + .agent-sessions-new-button-container { + /* hide new session button when stacked */ + display: none; + } } /* Sessions control: side by side */ @@ -105,6 +110,10 @@ border-left: 1px solid var(--vscode-panel-border); } } + + .agent-sessions-new-button-container { + padding: 8px 12px; + } } /* diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 5e344ac04c8dc..f135d3353eb65 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -387,6 +387,17 @@ export interface IChatTerminalToolInvocationData { /** The cd prefix to prepend back when user edits */ cdPrefix?: string; }; + /** + * Overrides to apply to the presentation of the tool call only, but not actually change the + * command that gets run. For example, python -c "print('hello')" can be presented as just + * the Python code with Python syntax highlighting. + */ + presentationOverrides?: { + /** The command line to display in the UI */ + commandLine: string; + /** The language for syntax highlighting */ + language: string; + }; /** Message for model recommending the use of an alternative tool */ alternativeRecommendation?: string; language: string; diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index b441782ef7b1a..ba407ce37febf 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -35,7 +35,6 @@ export enum ChatConfiguration { ShowCodeBlockProgressAnimation = 'chat.agent.codeBlockProgress', RestoreLastPanelSession = 'chat.restoreLastPanelSession', ExitAfterDelegation = 'chat.exitAfterDelegation', - SuspendThrottling = 'chat.suspendThrottling', } /** diff --git a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts index 8b7545f1bebf6..3b3dc98eccd4f 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts @@ -144,7 +144,7 @@ export class ChatToolInvocation implements IChatToolInvocation { * Transition from streaming state to prepared/executing state. * Called when the full tool call is ready. */ - public transitionFromStreaming(preparedInvocation: IPreparedToolInvocation | undefined, parameters: unknown): void { + public transitionFromStreaming(preparedInvocation: IPreparedToolInvocation | undefined, parameters: unknown, autoConfirmed: ConfirmedReason | undefined): void { const currentState = this._state.get(); if (currentState.type !== IChatToolInvocation.StateKind.Streaming) { return; // Only transition from streaming state @@ -168,8 +168,29 @@ export class ChatToolInvocation implements IChatToolInvocation { this.toolSpecificData = preparedInvocation.toolSpecificData; } + const confirm = (reason: ConfirmedReason) => { + if (reason.type === ToolConfirmKind.Denied || reason.type === ToolConfirmKind.Skipped) { + this._state.set({ + type: IChatToolInvocation.StateKind.Cancelled, + reason: reason.type, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); + } else { + this._state.set({ + type: IChatToolInvocation.StateKind.Executing, + confirmed: reason, + progress: this._progress, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); + } + }; + // Transition to the appropriate state - if (!this.confirmationMessages?.title) { + if (autoConfirmed) { + confirm(autoConfirmed); + } if (!this.confirmationMessages?.title) { this._state.set({ type: IChatToolInvocation.StateKind.Executing, confirmed: { type: ToolConfirmKind.ConfirmationNotNeeded, reason: this.confirmationMessages?.confirmationNotNeededReason }, @@ -182,24 +203,7 @@ export class ChatToolInvocation implements IChatToolInvocation { type: IChatToolInvocation.StateKind.WaitingForConfirmation, parameters: this.parameters, confirmationMessages: this.confirmationMessages, - confirm: reason => { - if (reason.type === ToolConfirmKind.Denied || reason.type === ToolConfirmKind.Skipped) { - this._state.set({ - type: IChatToolInvocation.StateKind.Cancelled, - reason: reason.type, - parameters: this.parameters, - confirmationMessages: this.confirmationMessages, - }, undefined); - } else { - this._state.set({ - type: IChatToolInvocation.StateKind.Executing, - confirmed: reason, - progress: this._progress, - parameters: this.parameters, - confirmationMessages: this.confirmationMessages, - }, undefined); - } - } + confirm, }, undefined); } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 2be3f234d1fa4..83c2c30e2d553 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -27,7 +27,6 @@ import { ITelemetryService } from '../../../../../../platform/telemetry/common/t import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js'; import { IVariableReference } from '../../chatModes.js'; import { PromptsConfig } from '../config/config.js'; -import { IDefaultAccountService } from '../../../../../../platform/defaultAccount/common/defaultAccount.js'; import { getCleanPromptName, IResolvedPromptFile, PromptFileSource } from '../config/promptFileLocations.js'; import { PROMPT_LANGUAGE_ID, PromptsType, getPromptsTypeForLanguageId } from '../promptTypes.js'; import { PromptFilesLocator } from '../utils/promptFilesLocator.js'; @@ -129,7 +128,6 @@ export class PromptsService extends Disposable implements IPromptsService { @IFilesConfigurationService private readonly filesConfigService: IFilesConfigurationService, @IStorageService private readonly storageService: IStorageService, @IExtensionService private readonly extensionService: IExtensionService, - @IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IChatPromptContentStore private readonly chatPromptContentStore: IChatPromptContentStore ) { @@ -756,9 +754,7 @@ export class PromptsService extends Disposable implements IPromptsService { public async findAgentSkills(token: CancellationToken): Promise { const useAgentSkills = this.configurationService.getValue(PromptsConfig.USE_AGENT_SKILLS); - const defaultAccount = await this.defaultAccountService.getDefaultAccount(); - const previewFeaturesEnabled = defaultAccount?.chat_preview_features_enabled ?? true; - if (useAgentSkills && previewFeaturesEnabled) { + if (useAgentSkills) { const result: IAgentSkill[] = []; const seenNames = new Set(); const skillTypes = new Map(); diff --git a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts index e4c7c9cfbab0e..dc0d3a78777c1 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts @@ -12,7 +12,6 @@ import { ipcRenderer } from '../../../../base/parts/sandbox/electron-browser/glo import { localize } from '../../../../nls.js'; import { registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -27,7 +26,7 @@ import { ILifecycleService, ShutdownReason } from '../../../services/lifecycle/c import { ACTION_ID_NEW_CHAT, CHAT_OPEN_ACTION_ID, IChatViewOpenOptions } from '../browser/actions/chatActions.js'; import { IChatWidgetService } from '../browser/chat.js'; import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; -import { ChatConfiguration, ChatModeKind } from '../common/constants.js'; +import { ChatModeKind } from '../common/constants.js'; import { IChatService } from '../common/chatService/chatService.js'; import { registerChatDeveloperActions } from './actions/chatDeveloperActions.js'; import { registerChatExportZipAction } from './actions/chatExportZip.js'; @@ -99,15 +98,10 @@ class ChatSuspendThrottlingHandler extends Disposable { constructor( @INativeHostService nativeHostService: INativeHostService, - @IChatService chatService: IChatService, - @IConfigurationService configurationService: IConfigurationService + @IChatService chatService: IChatService ) { super(); - if (!configurationService.getValue(ChatConfiguration.SuspendThrottling)) { - return; - } - this._register(autorun(reader => { const running = chatService.requestInProgressObs.read(reader); diff --git a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts new file mode 100644 index 0000000000000..f871099156480 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts @@ -0,0 +1,342 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { Location } from '../../../../../../editor/common/languages.js'; +import { getToolSpecificDataDescription, getResultDetailsDescription, getToolInvocationA11yDescription } from '../../../browser/accessibility/chatResponseAccessibleView.js'; +import { IChatExtensionsContent, IChatPullRequestContent, IChatSubagentToolInvocationData, IChatTerminalToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData } from '../../../common/chatService/chatService.js'; + +suite('ChatResponseAccessibleView', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('getToolSpecificDataDescription', () => { + test('returns empty string for undefined', () => { + assert.strictEqual(getToolSpecificDataDescription(undefined), ''); + }); + + test('returns command line for terminal data', () => { + const terminalData: IChatTerminalToolInvocationData = { + kind: 'terminal', + commandLine: { + original: 'npm install', + toolEdited: 'npm ci', + userEdited: 'npm install --save-dev' + }, + language: 'bash' + }; + // Should prefer userEdited over toolEdited over original + assert.strictEqual(getToolSpecificDataDescription(terminalData), 'npm install --save-dev'); + }); + + test('returns tool edited command for terminal data without user edit', () => { + const terminalData: IChatTerminalToolInvocationData = { + kind: 'terminal', + commandLine: { + original: 'npm install', + toolEdited: 'npm ci' + }, + language: 'bash' + }; + assert.strictEqual(getToolSpecificDataDescription(terminalData), 'npm ci'); + }); + + test('returns original command for terminal data without edits', () => { + const terminalData: IChatTerminalToolInvocationData = { + kind: 'terminal', + commandLine: { + original: 'npm install' + }, + language: 'bash' + }; + assert.strictEqual(getToolSpecificDataDescription(terminalData), 'npm install'); + }); + + test('returns description for subagent data', () => { + const subagentData: IChatSubagentToolInvocationData = { + kind: 'subagent', + agentName: 'TestAgent', + description: 'Running analysis', + prompt: 'Analyze the code' + }; + const result = getToolSpecificDataDescription(subagentData); + assert.ok(result.includes('TestAgent')); + assert.ok(result.includes('Running analysis')); + assert.ok(result.includes('Analyze the code')); + }); + + test('handles subagent with only description', () => { + const subagentData: IChatSubagentToolInvocationData = { + kind: 'subagent', + description: 'Running analysis' + }; + const result = getToolSpecificDataDescription(subagentData); + assert.strictEqual(result, 'Running analysis'); + }); + + test('returns extensions list for extensions data', () => { + const extensionsData: IChatExtensionsContent = { + kind: 'extensions', + extensions: ['eslint', 'prettier', 'typescript'] + }; + const result = getToolSpecificDataDescription(extensionsData); + assert.ok(result.includes('eslint')); + assert.ok(result.includes('prettier')); + assert.ok(result.includes('typescript')); + }); + + test('returns empty for empty extensions array', () => { + const extensionsData: IChatExtensionsContent = { + kind: 'extensions', + extensions: [] + }; + assert.strictEqual(getToolSpecificDataDescription(extensionsData), ''); + }); + + test('returns todo list description for todoList data', () => { + const todoData: IChatTodoListContent = { + kind: 'todoList', + sessionId: 'session-1', + todoList: [ + { id: '1', title: 'Task 1', description: 'Do something', status: 'in-progress' }, + { id: '2', title: 'Task 2', description: 'Do something else', status: 'completed' } + ] + }; + const result = getToolSpecificDataDescription(todoData); + assert.ok(result.includes('2 items')); + assert.ok(result.includes('Task 1')); + assert.ok(result.includes('in-progress')); + assert.ok(result.includes('Task 2')); + assert.ok(result.includes('completed')); + }); + + test('returns empty for empty todo list', () => { + const todoData: IChatTodoListContent = { + kind: 'todoList', + sessionId: 'session-1', + todoList: [] + }; + assert.strictEqual(getToolSpecificDataDescription(todoData), ''); + }); + + test('returns PR info for pullRequest data', () => { + const prData: IChatPullRequestContent = { + kind: 'pullRequest', + uri: URI.file('/test'), + title: 'Add new feature', + description: 'This PR adds a great feature', + author: 'testuser', + linkTag: '#123' + }; + const result = getToolSpecificDataDescription(prData); + assert.ok(result.includes('Add new feature')); + assert.ok(result.includes('testuser')); + }); + + test('returns raw input for input data (string)', () => { + const inputData: IChatToolInputInvocationData = { + kind: 'input', + rawInput: 'some input string' + }; + assert.strictEqual(getToolSpecificDataDescription(inputData), 'some input string'); + }); + + test('returns JSON stringified for input data (object)', () => { + const inputData: IChatToolInputInvocationData = { + kind: 'input', + rawInput: { key: 'value', nested: { data: 123 } } + }; + const result = getToolSpecificDataDescription(inputData); + assert.ok(result.includes('key')); + assert.ok(result.includes('value')); + }); + }); + + suite('getResultDetailsDescription', () => { + test('returns empty object for undefined', () => { + assert.deepStrictEqual(getResultDetailsDescription(undefined), {}); + }); + + test('returns files for URI array', () => { + const uris = [ + URI.file('/path/to/file1.ts'), + URI.file('/path/to/file2.ts') + ]; + const result = getResultDetailsDescription(uris); + assert.ok(result.files); + assert.strictEqual(result.files!.length, 2); + assert.ok(result.files![0].includes('file1.ts')); + assert.ok(result.files![1].includes('file2.ts')); + }); + + test('returns files for Location array', () => { + const locations: Location[] = [ + { uri: URI.file('/path/to/file1.ts'), range: new Range(1, 1, 10, 1) }, + { uri: URI.file('/path/to/file2.ts'), range: new Range(5, 1, 15, 1) } + ]; + const result = getResultDetailsDescription(locations); + assert.ok(result.files); + assert.strictEqual(result.files!.length, 2); + }); + + test('returns input and isError for IToolResultInputOutputDetails', () => { + const details = { + input: 'create_file path=/test/file.ts', + output: [], + isError: false + }; + const result = getResultDetailsDescription(details); + assert.strictEqual(result.input, 'create_file path=/test/file.ts'); + assert.strictEqual(result.isError, false); + }); + + test('returns isError true for errored IToolResultInputOutputDetails', () => { + const details = { + input: 'create_file path=/test/file.ts', + output: [], + isError: true + }; + const result = getResultDetailsDescription(details); + assert.strictEqual(result.isError, true); + }); + }); + + suite('getToolInvocationA11yDescription', () => { + test('returns invocation message when not complete', () => { + const result = getToolInvocationA11yDescription( + 'Creating file', + 'Created file', + undefined, + undefined, + false + ); + assert.strictEqual(result, 'Creating file'); + }); + + test('returns past tense message when complete', () => { + const result = getToolInvocationA11yDescription( + 'Creating file', + 'Created file', + undefined, + undefined, + true + ); + assert.strictEqual(result, 'Created file'); + }); + + test('includes tool-specific data description', () => { + const terminalData: IChatTerminalToolInvocationData = { + kind: 'terminal', + commandLine: { original: 'npm test' }, + language: 'bash' + }; + const result = getToolInvocationA11yDescription( + 'Running command', + 'Ran command', + terminalData, + undefined, + true + ); + assert.ok(result.includes('Ran command')); + assert.ok(result.includes('npm test')); + }); + + test('includes files from result details when complete', () => { + const uris = [ + URI.file('/path/to/file1.ts'), + URI.file('/path/to/file2.ts') + ]; + const result = getToolInvocationA11yDescription( + 'Creating files', + 'Created files', + undefined, + uris, + true + ); + assert.ok(result.includes('Created files')); + assert.ok(result.includes('file1.ts')); + assert.ok(result.includes('file2.ts')); + }); + + test('includes error status when result has error', () => { + const details = { + input: 'create_file path=/test/file.ts', + output: [], + isError: true + }; + const result = getToolInvocationA11yDescription( + 'Creating file', + 'Created file', + undefined, + details, + true + ); + assert.ok(result.includes('Errored')); + }); + + test('does not show input when tool-specific data is provided', () => { + const terminalData: IChatTerminalToolInvocationData = { + kind: 'terminal', + commandLine: { original: 'npm test' }, + language: 'bash' + }; + const details = { + input: 'some redundant input', + output: [], + isError: false + }; + const result = getToolInvocationA11yDescription( + 'Running command', + 'Ran command', + terminalData, + details, + true + ); + // Should have tool-specific data but not the "Input:" label + assert.ok(result.includes('npm test')); + assert.ok(!result.includes('Input:')); + }); + + test('shows input when no tool-specific data', () => { + const details = { + input: 'apply_patch file=/test/file.ts', + output: [], + isError: false + }; + const result = getToolInvocationA11yDescription( + 'Applying patch', + 'Applied patch', + undefined, + details, + true + ); + assert.ok(result.includes('Applied patch')); + assert.ok(result.includes('Input:')); + assert.ok(result.includes('apply_patch')); + }); + + test('handles all parts together', () => { + const subagentData: IChatSubagentToolInvocationData = { + kind: 'subagent', + agentName: 'CodeReviewer', + description: 'Reviewing code changes' + }; + const uris = [URI.file('/src/test.ts')]; + const result = getToolInvocationA11yDescription( + 'Starting code review', + 'Completed code review', + subagentData, + uris, + true + ); + assert.ok(result.includes('Completed code review')); + assert.ok(result.includes('CodeReviewer')); + assert.ok(result.includes('Reviewing code changes')); + assert.ok(result.includes('test.ts')); + }); + }); +}); 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 ac42dfb5c4bfa..e24a22e080245 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 @@ -47,8 +47,6 @@ import { InMemoryStorageService, IStorageService } from '../../../../../../../pl import { IPathService } from '../../../../../../services/path/common/pathService.js'; import { IFileMatch, IFileQuery, ISearchService } from '../../../../../../services/search/common/search.js'; import { IExtensionService } from '../../../../../../services/extensions/common/extensions.js'; -import { IDefaultAccountService } from '../../../../../../../platform/defaultAccount/common/defaultAccount.js'; -import { IDefaultAccount } from '../../../../../../../base/common/defaultAccount.js'; suite('PromptsService', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -84,10 +82,6 @@ suite('PromptsService', () => { activateByEvent: () => Promise.resolve() }); - instaService.stub(IDefaultAccountService, { - getDefaultAccount: () => Promise.resolve({ chat_preview_features_enabled: true } as IDefaultAccount) - }); - fileService = disposables.add(instaService.createInstance(FileService)); instaService.stub(IFileService, fileService); @@ -2360,42 +2354,6 @@ suite('PromptsService', () => { assert.strictEqual(result, undefined); }); - test('should return undefined when chat_preview_features_enabled is false', async () => { - testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); - instaService.stub(IDefaultAccountService, { - getDefaultAccount: () => Promise.resolve({ chat_preview_features_enabled: false } as IDefaultAccount) - }); - - // Recreate service with new stub - service = disposables.add(instaService.createInstance(PromptsService)); - - const result = await service.findAgentSkills(CancellationToken.None); - assert.strictEqual(result, undefined); - - // Restore default stub for other tests - instaService.stub(IDefaultAccountService, { - getDefaultAccount: () => Promise.resolve({ chat_preview_features_enabled: true } as IDefaultAccount) - }); - }); - - test('should return undefined when USE_AGENT_SKILLS is enabled but chat_preview_features_enabled is false', async () => { - testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); - instaService.stub(IDefaultAccountService, { - getDefaultAccount: () => Promise.resolve({ chat_preview_features_enabled: false } as IDefaultAccount) - }); - - // Recreate service with new stub - service = disposables.add(instaService.createInstance(PromptsService)); - - const result = await service.findAgentSkills(CancellationToken.None); - assert.strictEqual(result, undefined); - - // Restore default stub for other tests - instaService.stub(IDefaultAccountService, { - getDefaultAccount: () => Promise.resolve({ chat_preview_features_enabled: true } as IDefaultAccount) - }); - }); - test('should find skills in workspace and user home', async () => { testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 6c86e01fcd55d..fdb06fe25e334 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -279,7 +279,7 @@ export class BreakpointsView extends ViewPane { } })); - // Track collapsed state and update size (items are collapsed by default) + // Track collapsed state and update size (items are expanded by default) this._register(this.tree.onDidChangeCollapseState(e => { const element = e.node.element; if (element instanceof BreakpointsFolderItem) { @@ -542,15 +542,9 @@ export class BreakpointsView extends ViewPane { result.push({ element: folderItem, incompressible: false, - collapsed: this.collapsedState.has(folderItem.getId()) || !this.collapsedState.has(`_init_${folderItem.getId()}`), + collapsed: this.collapsedState.has(folderItem.getId()), children }); - - // Mark as initialized (will be collapsed by default on first render) - if (!this.collapsedState.has(`_init_${folderItem.getId()}`)) { - this.collapsedState.add(`_init_${folderItem.getId()}`); - this.collapsedState.add(folderItem.getId()); - } } } else { // Flat mode - just add all source breakpoints diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsChart.ts b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsChart.ts new file mode 100644 index 0000000000000..b02f21ebd853b --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsChart.ts @@ -0,0 +1,285 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $ } from '../../../../../base/browser/dom.js'; +import { localize } from '../../../../../nls.js'; +import { asCssVariable } from '../../../../../platform/theme/common/colorUtils.js'; +import { chartsBlue, chartsForeground, chartsLines } from '../../../../../platform/theme/common/colorRegistry.js'; + +export interface ISessionData { + startTime: number; + typedCharacters: number; + aiCharacters: number; + acceptedInlineSuggestions: number | undefined; + chatEditCount: number | undefined; +} + +export interface IDailyAggregate { + date: string; // ISO date string (YYYY-MM-DD) + displayDate: string; // Formatted for display + aiRate: number; + totalAiChars: number; + totalTypedChars: number; + inlineSuggestions: number; + chatEdits: number; + sessionCount: number; +} + +export type ChartViewMode = 'days' | 'sessions'; + +export function aggregateSessionsByDay(sessions: readonly ISessionData[]): IDailyAggregate[] { + const dayMap = new Map(); + + for (const session of sessions) { + const date = new Date(session.startTime); + const isoDate = date.toISOString().split('T')[0]; + const displayDate = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + + let aggregate = dayMap.get(isoDate); + if (!aggregate) { + aggregate = { + date: isoDate, + displayDate, + aiRate: 0, + totalAiChars: 0, + totalTypedChars: 0, + inlineSuggestions: 0, + chatEdits: 0, + sessionCount: 0, + }; + dayMap.set(isoDate, aggregate); + } + + aggregate.totalAiChars += session.aiCharacters; + aggregate.totalTypedChars += session.typedCharacters; + aggregate.inlineSuggestions += session.acceptedInlineSuggestions ?? 0; + aggregate.chatEdits += session.chatEditCount ?? 0; + aggregate.sessionCount += 1; + } + + // Calculate AI rate for each day + for (const aggregate of dayMap.values()) { + const total = aggregate.totalAiChars + aggregate.totalTypedChars; + aggregate.aiRate = total > 0 ? aggregate.totalAiChars / total : 0; + } + + // Sort by date + return Array.from(dayMap.values()).sort((a, b) => a.date.localeCompare(b.date)); +} + +export interface IAiStatsChartOptions { + sessions: readonly ISessionData[]; + viewMode: ChartViewMode; +} + +export function createAiStatsChart( + options: IAiStatsChartOptions +): HTMLElement { + const { sessions: sessionsData, viewMode: mode } = options; + + const width = 280; + const height = 100; + const margin = { top: 10, right: 10, bottom: 25, left: 30 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const container = $('.ai-stats-chart-container'); + container.style.position = 'relative'; + container.style.marginTop = '8px'; + + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', `${width}px`); + svg.setAttribute('height', `${height}px`); + svg.setAttribute('viewBox', `0 0 ${width} ${height}`); + svg.style.display = 'block'; + container.appendChild(svg); + + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + g.setAttribute('transform', `translate(${margin.left},${margin.top})`); + svg.appendChild(g); + + if (sessionsData.length === 0) { + const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + text.setAttribute('x', `${innerWidth / 2}`); + text.setAttribute('y', `${innerHeight / 2}`); + text.setAttribute('text-anchor', 'middle'); + text.setAttribute('fill', asCssVariable(chartsForeground)); + text.setAttribute('font-size', '11px'); + text.textContent = localize('noData', "No data yet"); + g.appendChild(text); + return container; + } + + // Draw axes + const xAxisLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + xAxisLine.setAttribute('x1', '0'); + xAxisLine.setAttribute('y1', `${innerHeight}`); + xAxisLine.setAttribute('x2', `${innerWidth}`); + xAxisLine.setAttribute('y2', `${innerHeight}`); + xAxisLine.setAttribute('stroke', asCssVariable(chartsLines)); + xAxisLine.setAttribute('stroke-width', '1px'); + g.appendChild(xAxisLine); + + const yAxisLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + yAxisLine.setAttribute('x1', '0'); + yAxisLine.setAttribute('y1', '0'); + yAxisLine.setAttribute('x2', '0'); + yAxisLine.setAttribute('y2', `${innerHeight}`); + yAxisLine.setAttribute('stroke', asCssVariable(chartsLines)); + yAxisLine.setAttribute('stroke-width', '1px'); + g.appendChild(yAxisLine); + + // Y-axis labels (0%, 50%, 100%) + for (const pct of [0, 50, 100]) { + const y = innerHeight - (pct / 100) * innerHeight; + const label = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + label.setAttribute('x', '-4'); + label.setAttribute('y', `${y + 3}`); + label.setAttribute('text-anchor', 'end'); + label.setAttribute('fill', asCssVariable(chartsForeground)); + label.setAttribute('font-size', '9px'); + label.textContent = `${pct}%`; + g.appendChild(label); + + if (pct > 0) { + const gridLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + gridLine.setAttribute('x1', '0'); + gridLine.setAttribute('y1', `${y}`); + gridLine.setAttribute('x2', `${innerWidth}`); + gridLine.setAttribute('y2', `${y}`); + gridLine.setAttribute('stroke', asCssVariable(chartsLines)); + gridLine.setAttribute('stroke-width', '0.5px'); + gridLine.setAttribute('stroke-dasharray', '2,2'); + g.appendChild(gridLine); + } + } + + if (mode === 'days') { + renderDaysView(); + } else { + renderSessionsView(); + } + + function renderDaysView() { + const dailyData = aggregateSessionsByDay(sessionsData); + const barCount = dailyData.length; + const barWidth = Math.min(20, (innerWidth - (barCount - 1) * 2) / barCount); + const gap = 2; + const totalBarSpace = barCount * barWidth + (barCount - 1) * gap; + const startX = (innerWidth - totalBarSpace) / 2; + + // Calculate which labels to show based on available space + // Each label needs roughly 40px of space to not overlap + const minLabelSpacing = 40; + const totalWidth = totalBarSpace; + const maxLabels = Math.max(2, Math.floor(totalWidth / minLabelSpacing)); + const labelStep = Math.max(1, Math.ceil(barCount / maxLabels)); + + dailyData.forEach((day, i) => { + const x = startX + i * (barWidth + gap); + const barHeight = day.aiRate * innerHeight; + const y = innerHeight - barHeight; + + // Bar for AI rate + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.setAttribute('x', `${x}`); + rect.setAttribute('y', `${y}`); + rect.setAttribute('width', `${barWidth}`); + rect.setAttribute('height', `${Math.max(1, barHeight)}`); + rect.setAttribute('fill', asCssVariable(chartsBlue)); + rect.setAttribute('rx', '2'); + g.appendChild(rect); + + // X-axis label - only show at calculated intervals to avoid overlap + const isFirst = i === 0; + const isLast = i === barCount - 1; + const isAtInterval = i % labelStep === 0; + + if (isFirst || isLast || (isAtInterval && barCount > 2)) { + // Skip middle labels if they would be too close to first/last + if (!isFirst && !isLast) { + const distFromFirst = i * (barWidth + gap); + const distFromLast = (barCount - 1 - i) * (barWidth + gap); + if (distFromFirst < minLabelSpacing || distFromLast < minLabelSpacing) { + return; // Skip this label + } + } + + const label = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + label.setAttribute('x', `${x + barWidth / 2}`); + label.setAttribute('y', `${innerHeight + 12}`); + label.setAttribute('text-anchor', 'middle'); + label.setAttribute('fill', asCssVariable(chartsForeground)); + label.setAttribute('font-size', '8px'); + label.textContent = day.displayDate; + g.appendChild(label); + } + }); + } + + function renderSessionsView() { + const sessionCount = sessionsData.length; + const barWidth = Math.min(8, (innerWidth - (sessionCount - 1) * 1) / sessionCount); + const gap = 1; + const totalBarSpace = sessionCount * barWidth + (sessionCount - 1) * gap; + const startX = (innerWidth - totalBarSpace) / 2; + + sessionsData.forEach((session, i) => { + const total = session.aiCharacters + session.typedCharacters; + const aiRate = total > 0 ? session.aiCharacters / total : 0; + const x = startX + i * (barWidth + gap); + const barHeight = aiRate * innerHeight; + const y = innerHeight - barHeight; + + // Bar for AI rate + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.setAttribute('x', `${x}`); + rect.setAttribute('y', `${y}`); + rect.setAttribute('width', `${barWidth}`); + rect.setAttribute('height', `${Math.max(1, barHeight)}`); + rect.setAttribute('fill', asCssVariable(chartsBlue)); + rect.setAttribute('rx', '1'); + g.appendChild(rect); + }); + + // X-axis labels: only show first and last to avoid overlap + // Each label is roughly 40px wide (e.g., "Jan 15") + const minLabelSpacing = 40; + + if (sessionCount === 0) { + return; + } + + // Always show first label + const firstSession = sessionsData[0]; + const firstX = startX; + const firstDate = new Date(firstSession.startTime); + const firstLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + firstLabel.setAttribute('x', `${firstX + barWidth / 2}`); + firstLabel.setAttribute('y', `${innerHeight + 12}`); + firstLabel.setAttribute('text-anchor', 'start'); + firstLabel.setAttribute('fill', asCssVariable(chartsForeground)); + firstLabel.setAttribute('font-size', '8px'); + firstLabel.textContent = firstDate.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + g.appendChild(firstLabel); + + // Show last label if there's enough space and more than 1 session + if (sessionCount > 1 && totalBarSpace >= minLabelSpacing) { + const lastSession = sessionsData[sessionCount - 1]; + const lastX = startX + (sessionCount - 1) * (barWidth + gap); + const lastDate = new Date(lastSession.startTime); + const lastLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + lastLabel.setAttribute('x', `${lastX + barWidth / 2}`); + lastLabel.setAttribute('y', `${innerHeight + 12}`); + lastLabel.setAttribute('text-anchor', 'end'); + lastLabel.setAttribute('fill', asCssVariable(chartsForeground)); + lastLabel.setAttribute('font-size', '8px'); + lastLabel.textContent = lastDate.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + g.appendChild(lastLabel); + } + } + + return container; +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsFeature.ts b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsFeature.ts index da6c2ea7955ed..922e0ac5acdbc 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsFeature.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsFeature.ts @@ -113,6 +113,15 @@ export class AiStatsFeature extends Disposable { return val.sessions.length; }); + public readonly sessions = derived(this, r => { + this._dataVersion.read(r); + const val = this._data.getValue(); + if (!val) { + return []; + } + return val.sessions; + }); + public readonly acceptedInlineSuggestionsToday = derived(this, r => { this._dataVersion.read(r); const val = this._data.getValue(); diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar.ts b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar.ts index 16248f06f26ec..9838eb00d44be 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar.ts @@ -9,7 +9,7 @@ import { IAction } from '../../../../../base/common/actions.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { createHotClass } from '../../../../../base/common/hotReloadHelpers.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { autorun, derived } from '../../../../../base/common/observable.js'; +import { autorun, derived, observableValue } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { localize } from '../../../../../nls.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; @@ -18,11 +18,14 @@ import { ITelemetryService } from '../../../../../platform/telemetry/common/tele import { IStatusbarService, StatusbarAlignment } from '../../../../services/statusbar/browser/statusbar.js'; import { AI_STATS_SETTING_ID } from '../settingIds.js'; import type { AiStatsFeature } from './aiStatsFeature.js'; +import { ChartViewMode, createAiStatsChart } from './aiStatsChart.js'; import './media.css'; export class AiStatsStatusBar extends Disposable { public static readonly hot = createHotClass(this); + private readonly _chartViewMode = observableValue(this, 'days'); + constructor( private readonly _aiStatsFeature: AiStatsFeature, @IStatusbarService private readonly _statusbarService: IStatusbarService, @@ -129,7 +132,7 @@ export class AiStatsStatusBar extends Disposable { n.div({ class: 'header', style: { - minWidth: '200px', + minWidth: '280px', } }, [ @@ -154,28 +157,89 @@ export class AiStatsStatusBar extends Disposable { n.div({ style: { flex: 1, paddingRight: '4px' } }, [ localize('text1', "AI vs Typing Average: {0}", aiRatePercent.get()), ]), - /* - TODO: Write article that explains the ratio and link to it. - - n.div({ style: { marginLeft: 'auto' } }, actionBar([ - { - action: { - id: 'aiStatsStatusBar.openSettings', - label: '', - enabled: true, - run: () => { }, - class: ThemeIcon.asClassName(Codicon.info), - tooltip: '' - }, - options: { icon: true, label: true, } - } - ]))*/ ]), n.div({ style: { flex: 1, paddingRight: '4px' } }, [ localize('text2', "Accepted inline suggestions today: {0}", this._aiStatsFeature.acceptedInlineSuggestionsToday.get()), ]), + + // Chart section + n.div({ + style: { + marginTop: '8px', + borderTop: '1px solid var(--vscode-widget-border)', + paddingTop: '8px', + } + }, [ + // Chart header with toggle + n.div({ + class: 'header', + style: { + display: 'flex', + alignItems: 'center', + marginBottom: '4px', + } + }, [ + n.div({ style: { flex: 1 } }, [ + this._chartViewMode.map(mode => + mode === 'days' + ? localize('chartHeaderDays', "AI Rate by Day") + : localize('chartHeaderSessions', "AI Rate by Session") + ) + ]), + n.div({ + class: 'chart-view-toggle', + style: { marginLeft: 'auto', display: 'flex', gap: '2px' } + }, [ + this._createToggleButton('days', localize('viewByDays', "Days"), Codicon.calendar), + this._createToggleButton('sessions', localize('viewBySessions', "Sessions"), Codicon.listFlat), + ]) + ]), + + // Chart container + derived(reader => { + const sessions = this._aiStatsFeature.sessions.read(reader); + const viewMode = this._chartViewMode.read(reader); + return n.div({ + ref: (container) => { + const chart = createAiStatsChart({ + sessions, + viewMode, + }); + container.appendChild(chart); + } + }); + }), + ]), ]); } + + private _createToggleButton(mode: ChartViewMode, tooltip: string, icon: ThemeIcon) { + return derived(reader => { + const currentMode = this._chartViewMode.read(reader); + const isActive = currentMode === mode; + + return n.div({ + class: ['chart-toggle-button', isActive ? 'active' : ''], + style: { + padding: '2px 4px', + borderRadius: '3px', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + onclick: () => { + this._chartViewMode.set(mode, undefined); + }, + title: tooltip, + }, [ + n.div({ + class: ThemeIcon.asClassName(icon), + style: { fontSize: '14px' } + }) + ]); + }); + } } function actionBar(actions: { action: IAction; options: IActionOptions }[], options?: IActionBarOptions) { diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editStats/media.css b/src/vs/workbench/contrib/editTelemetry/browser/editStats/media.css index e0eaa8eff4a2c..3668b5565fdeb 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editStats/media.css +++ b/src/vs/workbench/contrib/editTelemetry/browser/editStats/media.css @@ -33,6 +33,39 @@ margin-bottom: 5px; } + /* Chart toggle buttons */ + .chart-view-toggle { + display: flex; + gap: 2px; + } + + .chart-toggle-button { + padding: 2px 6px; + border-radius: 3px; + cursor: pointer; + opacity: 0.6; + transition: opacity 0.15s, background-color 0.15s; + } + + .chart-toggle-button:hover { + opacity: 1; + background-color: var(--vscode-toolbar-hoverBackground); + } + + .chart-toggle-button.active { + opacity: 1; + background-color: var(--vscode-toolbar-activeBackground); + } + + /* Chart container */ + .ai-stats-chart-container { + margin-top: 4px; + } + + .ai-stats-chart-container svg { + overflow: visible; + } + /* Setup for New User */ .setup .chat-feature-container { diff --git a/src/vs/workbench/contrib/terminal/terminalContribExports.ts b/src/vs/workbench/contrib/terminal/terminalContribExports.ts index 59f7bae295e0d..1fd3bb600a07d 100644 --- a/src/vs/workbench/contrib/terminal/terminalContribExports.ts +++ b/src/vs/workbench/contrib/terminal/terminalContribExports.ts @@ -43,7 +43,7 @@ export const enum TerminalContribSettingId { AutoApprove = TerminalChatAgentToolsSettingId.AutoApprove, EnableAutoApprove = TerminalChatAgentToolsSettingId.EnableAutoApprove, ShellIntegrationTimeout = TerminalChatAgentToolsSettingId.ShellIntegrationTimeout, - OutputLocation = TerminalChatAgentToolsSettingId.OutputLocation + OutputLocation = TerminalChatAgentToolsSettingId.OutputLocation, } // HACK: Export some context key strings from `terminalContrib/` that are depended upon elsewhere. diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/commandFileWriteParser.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/commandFileWriteParser.ts new file mode 100644 index 0000000000000..09d14d75e422d --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/commandFileWriteParser.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Interface for command-specific file write parsers. + * Each parser is responsible for detecting when a specific command will write to files + * (beyond simple shell redirections which are handled separately via tree-sitter queries). + */ +export interface ICommandFileWriteParser { + /** + * The name of the command this parser handles (e.g., 'sed', 'tee'). + */ + readonly commandName: string; + + /** + * Checks if this parser can handle the given command text. + * Should return true only if the command would write to files. + * @param commandText The full text of a single command (not a pipeline). + */ + canHandle(commandText: string): boolean; + + /** + * Extracts the file paths that would be written to by this command. + * Should only be called if canHandle() returns true. + * @param commandText The full text of a single command (not a pipeline). + * @returns Array of file paths that would be modified. + */ + extractFileWrites(commandText: string): string[]; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/sedFileWriteParser.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/sedFileWriteParser.ts new file mode 100644 index 0000000000000..f1442781c7427 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/sedFileWriteParser.ts @@ -0,0 +1,212 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICommandFileWriteParser } from './commandFileWriteParser.js'; + +/** + * Parser for detecting file writes from `sed` commands using in-place editing. + * + * Handles: + * - `sed -i 's/foo/bar/' file.txt` (GNU) + * - `sed -i.bak 's/foo/bar/' file.txt` (GNU with backup suffix) + * - `sed -i '' 's/foo/bar/' file.txt` (macOS/BSD with empty backup suffix) + * - `sed --in-place 's/foo/bar/' file.txt` (GNU long form) + * - `sed --in-place=.bak 's/foo/bar/' file.txt` (GNU long form with backup) + * - `sed -I 's/foo/bar/' file.txt` (BSD case-insensitive variant) + */ +export class SedFileWriteParser implements ICommandFileWriteParser { + readonly commandName = 'sed'; + + canHandle(commandText: string): boolean { + // Check if this is a sed command + if (!commandText.match(/^sed\s+/)) { + return false; + } + + // Check for -i, -I, or --in-place flag + const inPlaceRegex = /(?:^|\s)(-[a-zA-Z]*[iI][a-zA-Z]*\S*|--in-place(?:=\S*)?|(-i|-I)\s*'[^']*'|(-i|-I)\s*"[^"]*")(?:\s|$)/; + return inPlaceRegex.test(commandText); + } + + extractFileWrites(commandText: string): string[] { + const tokens = this._tokenizeCommand(commandText); + return this._extractFileTargets(tokens); + } + + /** + * Tokenizes a command into individual arguments, handling quotes and escapes. + */ + private _tokenizeCommand(commandText: string): string[] { + const tokens: string[] = []; + let current = ''; + let inSingleQuote = false; + let inDoubleQuote = false; + let escaped = false; + + for (let i = 0; i < commandText.length; i++) { + const char = commandText[i]; + + if (escaped) { + current += char; + escaped = false; + continue; + } + + if (char === '\\' && !inSingleQuote) { + escaped = true; + current += char; + continue; + } + + if (char === '\'' && !inDoubleQuote) { + inSingleQuote = !inSingleQuote; + current += char; + continue; + } + + if (char === '"' && !inSingleQuote) { + inDoubleQuote = !inDoubleQuote; + current += char; + continue; + } + + if (/\s/.test(char) && !inSingleQuote && !inDoubleQuote) { + if (current) { + tokens.push(current); + current = ''; + } + continue; + } + + current += char; + } + + if (current) { + tokens.push(current); + } + + return tokens; + } + + /** + * Extracts file targets from tokenized sed command arguments. + * Files are generally the last non-option, non-script arguments. + */ + private _extractFileTargets(tokens: string[]): string[] { + if (tokens.length === 0 || tokens[0] !== 'sed') { + return []; + } + + const files: string[] = []; + let i = 1; // Skip 'sed' + let foundScript = false; + + while (i < tokens.length) { + const token = tokens[i]; + + // Long options + if (token.startsWith('--')) { + if (token === '--in-place' || token.startsWith('--in-place=')) { + // In-place flag (already verified we have one) + i++; + continue; + } + if (token === '--expression' || token === '--file') { + // Skip the option and its argument + i += 2; + foundScript = true; + continue; + } + if (token.startsWith('--expression=') || token.startsWith('--file=')) { + i++; + foundScript = true; + continue; + } + // Other long options like --sandbox, --debug, etc. + i++; + continue; + } + + // Short options + if (token.startsWith('-') && token.length > 1 && token[1] !== '-') { + // Could be combined flags like -ni or -i.bak + const flags = token.slice(1); + + // Check if this is -i with backup suffix attached (e.g., -i.bak) + const iIndex = flags.indexOf('i'); + const IIndex = flags.indexOf('I'); + const inPlaceIndex = iIndex >= 0 ? iIndex : IIndex; + + if (inPlaceIndex >= 0 && inPlaceIndex < flags.length - 1) { + // -i.bak style - backup suffix is attached + i++; + continue; + } + + // Check if -i or -I is the last flag and next token could be backup suffix + if ((flags.endsWith('i') || flags.endsWith('I')) && i + 1 < tokens.length) { + const nextToken = tokens[i + 1]; + // macOS/BSD style: -i '' or -i "" (empty string backup suffix) + // Only treat it as a backup suffix if it's empty or looks like a backup + // extension (starts with '.' and is short). Don't match sed scripts like 's/foo/bar/'. + if (nextToken === '\'\'' || nextToken === '""') { + i += 2; + continue; + } + // Check for quoted backup suffixes like '.bak' or ".backup" + if ((nextToken.startsWith('\'') && nextToken.endsWith('\'')) || (nextToken.startsWith('"') && nextToken.endsWith('"'))) { + const unquoted = nextToken.slice(1, -1); + // Backup suffixes typically start with '.' and are short extensions + if (unquoted.startsWith('.') && unquoted.length <= 10 && !unquoted.includes('/')) { + i += 2; + continue; + } + } + } + + // Check for -e or -f which take arguments + if (flags.includes('e') || flags.includes('f')) { + const eIndex = flags.indexOf('e'); + const fIndex = flags.indexOf('f'); + const optIndex = eIndex >= 0 ? eIndex : fIndex; + + // If -e or -f is not the last character, the rest of the token is the argument + if (optIndex < flags.length - 1) { + foundScript = true; + i++; + continue; + } + + // Otherwise, the next token is the argument + foundScript = true; + i += 2; + continue; + } + + i++; + continue; + } + + // Non-option argument + if (!foundScript) { + // First non-option is the script (unless -e/-f was used) + foundScript = true; + i++; + continue; + } + + // Subsequent non-option arguments are files + // Strip surrounding quotes from file path + let file = token; + if ((file.startsWith('\'') && file.endsWith('\'')) || (file.startsWith('"') && file.endsWith('"'))) { + file = file.slice(1, -1); + } + files.push(file); + i++; + } + + return files; + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts index 10d7575374d06..7f8f1e8d39ebb 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts @@ -207,9 +207,11 @@ export async function collectTerminalResults( taskLabelToTaskMap[dependencyTask._label] = dependencyTask; } - for (const instance of terminals) { - progress.report({ message: new MarkdownString(`Checking output for \`${instance.shellLaunchConfig.name ?? 'unknown'}\``) }); + // Process all terminals in parallel + const terminalNames = terminals.map(t => t.shellLaunchConfig.name ?? t.title ?? 'unknown'); + progress.report({ message: new MarkdownString(`Checking output for ${terminalNames.map(n => `\`${n}\``).join(', ')}`) }); + const terminalPromises = terminals.map(async (instance) => { let terminalTask = task; // For composite tasks, find the actual dependency task running in this terminal @@ -257,7 +259,7 @@ export async function collectTerminalResults( const outputMonitor = disposableStore.add(instantiationService.createInstance(OutputMonitor, execution, taskProblemPollFn, invocationContext, token, task._label)); await Event.toPromise(outputMonitor.onDidFinishCommand); const pollingResult = outputMonitor.pollingResult; - results.push({ + return { name: instance.shellLaunchConfig.name ?? instance.title ?? 'unknown', output: pollingResult?.output ?? '', pollDurationMs: pollingResult?.pollDurationMs ?? 0, @@ -271,8 +273,11 @@ export async function collectTerminalResults( inputToolManualShownCount: outputMonitor.outputMonitorTelemetryCounters.inputToolManualShownCount ?? 0, inputToolFreeFormInputShownCount: outputMonitor.outputMonitorTelemetryCounters.inputToolFreeFormInputShownCount ?? 0, inputToolFreeFormInputCount: outputMonitor.outputMonitorTelemetryCounters.inputToolFreeFormInputCount ?? 0, - }); - } + }; + }); + + const parallelResults = await Promise.all(terminalPromises); + results.push(...parallelResults); return results; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts index e9f18f15afac7..db7751a57b091 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts @@ -29,6 +29,14 @@ import { RunInTerminalTool, createRunInTerminalToolData } from './tools/runInTer import { CreateAndRunTaskTool, CreateAndRunTaskToolData } from './tools/task/createAndRunTaskTool.js'; import { GetTaskOutputTool, GetTaskOutputToolData } from './tools/task/getTaskOutputTool.js'; import { RunTaskTool, RunTaskToolData } from './tools/task/runTaskTool.js'; +import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js'; +import { ITerminalSandboxService, TerminalSandboxService } from '../common/terminalSandboxService.js'; + +// #region Services + +registerSingleton(ITerminalSandboxService, TerminalSandboxService, InstantiationType.Delayed); + +// #endregion Services class ShellIntegrationTimeoutMigrationContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'terminal.shellIntegrationTimeoutMigration'; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineFileWriteAnalyzer.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineFileWriteAnalyzer.ts index 0a5d98d1704c0..e86dcf79cfeea 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineFileWriteAnalyzer.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineFileWriteAnalyzer.ts @@ -47,13 +47,22 @@ export class CommandLineFileWriteAnalyzer extends Disposable implements ICommand private async _getFileWrites(options: ICommandLineAnalyzerOptions): Promise { let fileWrites: FileWrite[] = []; + + // Get file writes from redirections (via tree-sitter grammar) const capturedFileWrites = (await this._treeSitterCommandParser.getFileWrites(options.treeSitterLanguage, options.commandLine)) .map(this._mapNullDevice.bind(this, options)); - if (capturedFileWrites.length) { + + // Get file writes from command-specific parsers (e.g., sed -i in-place editing) + const commandFileWrites = (await this._treeSitterCommandParser.getCommandFileWrites(options.treeSitterLanguage, options.commandLine)) + .map(this._mapNullDevice.bind(this, options)); + + const allCapturedFileWrites = [...capturedFileWrites, ...commandFileWrites]; + + if (allCapturedFileWrites.length) { const cwd = options.cwd; if (cwd) { this._log('Detected cwd', cwd.toString()); - fileWrites = capturedFileWrites.map(e => { + fileWrites = allCapturedFileWrites.map(e => { if (e === nullDevice) { return e; } @@ -79,7 +88,7 @@ export class CommandLineFileWriteAnalyzer extends Disposable implements ICommand }); } else { this._log('Cwd could not be detected'); - fileWrites = capturedFileWrites; + fileWrites = allCapturedFileWrites; } } this._log('File writes detected', fileWrites.map(e => e.toString())); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/commandLinePresenter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/commandLinePresenter.ts new file mode 100644 index 0000000000000..3386df4491073 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/commandLinePresenter.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { OperatingSystem } from '../../../../../../../base/common/platform.js'; + +export interface ICommandLinePresenter { + /** + * Attempts to create a presentation for the given command line. + * Command line presenters allow displaying an extracted/transformed version + * of a command (e.g., Python code from `python -c "..."`) with appropriate + * syntax highlighting, while the actual command remains unchanged. + * + * @returns The presentation result if this presenter handles the command, undefined otherwise. + */ + present(options: ICommandLinePresenterOptions): ICommandLinePresenterResult | undefined; +} + +export interface ICommandLinePresenterOptions { + commandLine: string; + shell: string; + os: OperatingSystem; +} + +export interface ICommandLinePresenterResult { + /** + * The extracted/transformed command to display (e.g., the Python code). + */ + commandLine: string; + + /** + * The language ID for syntax highlighting (e.g., 'python'). + */ + language: string; + + /** + * A human-readable name for the language (e.g., 'Python') used in UI labels. + */ + languageDisplayName: string; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/nodeCommandLinePresenter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/nodeCommandLinePresenter.ts new file mode 100644 index 0000000000000..ec6a5125daa7e --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/nodeCommandLinePresenter.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { OperatingSystem } from '../../../../../../../base/common/platform.js'; +import { isPowerShell } from '../../runInTerminalHelpers.js'; +import type { ICommandLinePresenter, ICommandLinePresenterOptions, ICommandLinePresenterResult } from './commandLinePresenter.js'; + +/** + * Command line presenter for Node.js inline commands (`node -e "..."`). + * Extracts the JavaScript code and sets up JavaScript syntax highlighting. + */ +export class NodeCommandLinePresenter implements ICommandLinePresenter { + present(options: ICommandLinePresenterOptions): ICommandLinePresenterResult | undefined { + const extractedNode = extractNodeCommand(options.commandLine, options.shell, options.os); + if (extractedNode) { + return { + commandLine: extractedNode, + language: 'javascript', + languageDisplayName: 'Node.js', + }; + } + return undefined; + } +} + +/** + * Extracts the JavaScript code from a `node -e "..."` or `node -e '...'` command, + * returning the code with properly unescaped quotes. + * + * @param commandLine The full command line to parse + * @param shell The shell path (to determine quote escaping style) + * @param os The operating system + * @returns The extracted JavaScript code, or undefined if not a node -e/--eval command + */ +export function extractNodeCommand(commandLine: string, shell: string, os: OperatingSystem): string | undefined { + // Match node/nodejs -e/--eval "..." pattern (double quotes) + const doubleQuoteMatch = commandLine.match(/^node(?:js)?\s+(?:-e|--eval)\s+"(?.+)"$/s); + if (doubleQuoteMatch?.groups?.code) { + let jsCode = doubleQuoteMatch.groups.code.trim(); + + // Unescape quotes based on shell type + if (isPowerShell(shell, os)) { + // PowerShell uses backtick-quote (`") to escape quotes inside double-quoted strings + jsCode = jsCode.replace(/`"/g, '"'); + } else { + // Bash/sh/zsh use backslash-quote (\") + jsCode = jsCode.replace(/\\"/g, '"'); + } + + return jsCode; + } + + // Match node/nodejs -e/--eval '...' pattern (single quotes) + // Single quotes in bash/sh/zsh are literal - no escaping inside + // Single quotes in PowerShell are also literal + const singleQuoteMatch = commandLine.match(/^node(?:js)?\s+(?:-e|--eval)\s+'(?.+)'$/s); + if (singleQuoteMatch?.groups?.code) { + return singleQuoteMatch.groups.code.trim(); + } + + return undefined; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/pythonCommandLinePresenter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/pythonCommandLinePresenter.ts new file mode 100644 index 0000000000000..14aa3d6d14ffc --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLinePresenter/pythonCommandLinePresenter.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { OperatingSystem } from '../../../../../../../base/common/platform.js'; +import { isPowerShell } from '../../runInTerminalHelpers.js'; +import type { ICommandLinePresenter, ICommandLinePresenterOptions, ICommandLinePresenterResult } from './commandLinePresenter.js'; + +/** + * Command line presenter for Python inline commands (`python -c "..."`). + * Extracts the Python code and sets up Python syntax highlighting. + */ +export class PythonCommandLinePresenter implements ICommandLinePresenter { + present(options: ICommandLinePresenterOptions): ICommandLinePresenterResult | undefined { + const extractedPython = extractPythonCommand(options.commandLine, options.shell, options.os); + if (extractedPython) { + return { + commandLine: extractedPython, + language: 'python', + languageDisplayName: 'Python', + }; + } + return undefined; + } +} + +/** + * Extracts the Python code from a `python -c "..."` or `python -c '...'` command, + * returning the code with properly unescaped quotes. + * + * @param commandLine The full command line to parse + * @param shell The shell path (to determine quote escaping style) + * @param os The operating system + * @returns The extracted Python code, or undefined if not a python -c command + */ +export function extractPythonCommand(commandLine: string, shell: string, os: OperatingSystem): string | undefined { + // Match python/python3 -c "..." pattern (double quotes) + const doubleQuoteMatch = commandLine.match(/^python(?:3)?\s+-c\s+"(?.+)"$/s); + if (doubleQuoteMatch?.groups?.python) { + let pythonCode = doubleQuoteMatch.groups.python.trim(); + + // Unescape quotes based on shell type + if (isPowerShell(shell, os)) { + // PowerShell uses backtick-quote (`") to escape quotes inside double-quoted strings + pythonCode = pythonCode.replace(/`"/g, '"'); + } else { + // Bash/sh/zsh use backslash-quote (\") + pythonCode = pythonCode.replace(/\\"/g, '"'); + } + + return pythonCode; + } + + // Match python/python3 -c '...' pattern (single quotes) + // Single quotes in bash/sh/zsh are literal - no escaping inside + // Single quotes in PowerShell are also literal + const singleQuoteMatch = commandLine.match(/^python(?:3)?\s+-c\s+'(?.+)'$/s); + if (singleQuoteMatch?.groups?.python) { + return singleQuoteMatch.groups.python.trim(); + } + + return undefined; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index f251ee467d2f3..a9a95bde247ec 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -21,7 +21,7 @@ import { IConfigurationService } from '../../../../../../platform/configuration/ import { IInstantiationService, type ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; -import { TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; +import { ICommandDetectionCapability, TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; import { ITerminalLogService, ITerminalProfile } from '../../../../../../platform/terminal/common/terminal.js'; import { IRemoteAgentService } from '../../../../../services/remote/common/remoteAgentService.js'; import { TerminalToolConfirmationStorageKeys } from '../../../../chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js'; @@ -38,6 +38,9 @@ import { NoneExecuteStrategy } from '../executeStrategy/noneExecuteStrategy.js'; import { RichExecuteStrategy } from '../executeStrategy/richExecuteStrategy.js'; import { getOutput } from '../outputHelpers.js'; import { extractCdPrefix, isFish, isPowerShell, isWindowsPowerShell, isZsh } from '../runInTerminalHelpers.js'; +import type { ICommandLinePresenter } from './commandLinePresenter/commandLinePresenter.js'; +import { NodeCommandLinePresenter } from './commandLinePresenter/nodeCommandLinePresenter.js'; +import { PythonCommandLinePresenter } from './commandLinePresenter/pythonCommandLinePresenter.js'; import { RunInTerminalToolTelemetry } from '../runInTerminalToolTelemetry.js'; import { ShellIntegrationQuality, ToolTerminalCreator, type IToolTerminal } from '../toolTerminalCreator.js'; import { TreeSitterCommandParser, TreeSitterCommandParserLanguage } from '../treeSitterCommandParser.js'; @@ -46,6 +49,7 @@ import { CommandLineAutoApproveAnalyzer } from './commandLineAnalyzer/commandLin import { CommandLineFileWriteAnalyzer } from './commandLineAnalyzer/commandLineFileWriteAnalyzer.js'; import { OutputMonitor } from './monitoring/outputMonitor.js'; import { IPollingResult, OutputMonitorState } from './monitoring/types.js'; +import { ITerminalSandboxService } from '../../common/terminalSandboxService.js'; import { chatSessionResourceToId, LocalChatSessionUri } from '../../../../chat/common/model/chatUri.js'; import { URI } from '../../../../../../base/common/uri.js'; import type { ICommandLineRewriter } from './commandLineRewriter/commandLineRewriter.js'; @@ -281,6 +285,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { private readonly _commandLineRewriters: ICommandLineRewriter[]; private readonly _commandLineAnalyzers: ICommandLineAnalyzer[]; + private readonly _commandLinePresenters: ICommandLinePresenter[]; protected readonly _sessionTerminalAssociations = new ResourceMap(); @@ -310,6 +315,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { @ITerminalService private readonly _terminalService: ITerminalService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, + @ITerminalSandboxService private readonly _sandboxService: ITerminalSandboxService, ) { super(); @@ -330,6 +336,10 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { this._register(this._instantiationService.createInstance(CommandLineFileWriteAnalyzer, this._treeSitterCommandParser, (message, args) => this._logService.info(`RunInTerminalTool#CommandLineFileWriteAnalyzer: ${message}`, args))), this._register(this._instantiationService.createInstance(CommandLineAutoApproveAnalyzer, this._treeSitterCommandParser, this._telemetry, (message, args) => this._logService.info(`RunInTerminalTool#CommandLineAutoApproveAnalyzer: ${message}`, args))), ]; + this._commandLinePresenters = [ + new NodeCommandLinePresenter(), + new PythonCommandLinePresenter(), + ]; // Clear out warning accepted state if the setting is disabled this._register(Event.runAndSubscribe(this._configurationService.onDidChangeConfiguration, e => { @@ -338,6 +348,15 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { this._storageService.remove(TerminalToolConfirmationStorageKeys.TerminalAutoApproveWarningAccepted, StorageScope.APPLICATION); } } + // If terminal sandbox settings changed, update sandbox config. + if ( + e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled) || + e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork) || + e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem) || + e?.affectsConfiguration(TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem) + ) { + this._sandboxService.setNeedsForceUpdateConfigFile(); + } })); // Restore terminal associations from storage @@ -420,6 +439,15 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { }; } + // If in sandbox mode, skip confirmation logic. In sandbox mode, commands are run in a restricted environment and explicit + // user confirmation is not required. + if (this._sandboxService.isEnabled()) { + toolSpecificData.autoApproveInfo = new MarkdownString(localize('autoApprove.sandbox', 'In sandbox mode')); + return { + toolSpecificData + }; + } + // Determine auto approval, this happens even when auto approve is off to that reasoning // can be reviewed in the terminal channel. It also allows gauging the effective set of // commands that would be auto approved if it were enabled. @@ -523,17 +551,40 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { }; confirmationTitle = args.isBackground - ? localize('runInTerminal.background.inDirectory', "Run `{0}` command in `{1}`? (background terminal)", shellType, directoryLabel) - : localize('runInTerminal.inDirectory', "Run `{0}` command in `{1}`?", shellType, directoryLabel); + ? localize('runInTerminal.background.inDirectory', "Run `{0}` command in background within `{1}`?", shellType, directoryLabel) + : localize('runInTerminal.inDirectory', "Run `{0}` command within `{1}`?", shellType, directoryLabel); } else { toolSpecificData.confirmation = { commandLine: commandToDisplay, }; confirmationTitle = args.isBackground - ? localize('runInTerminal.background', "Run `{0}` command? (background terminal)", shellType) + ? localize('runInTerminal.background', "Run `{0}` command in background?", shellType) : localize('runInTerminal', "Run `{0}` command?", shellType); } + // Check for presentation overrides (e.g., Python -c command extraction) + // Use the command after cd prefix extraction if available, since that's what's displayed in the editor + const commandForPresenter = extractedCd?.command ?? commandToDisplay; + for (const presenter of this._commandLinePresenters) { + const presenterResult = presenter.present({ commandLine: commandForPresenter, shell, os }); + if (presenterResult) { + toolSpecificData.presentationOverrides = { + commandLine: presenterResult.commandLine, + language: presenterResult.language, + }; + if (extractedCd && toolSpecificData.confirmation?.cwdLabel) { + confirmationTitle = args.isBackground + ? localize('runInTerminal.presentationOverride.background.inDirectory', "Run `{0}` command in `{1}` in background within `{2}`?", presenterResult.languageDisplayName, shellType, toolSpecificData.confirmation.cwdLabel) + : localize('runInTerminal.presentationOverride.inDirectory', "Run `{0}` command in `{1}` within `{2}`?", presenterResult.languageDisplayName, shellType, toolSpecificData.confirmation.cwdLabel); + } else { + confirmationTitle = args.isBackground + ? localize('runInTerminal.presentationOverride.background', "Run `{0}` command in `{1}` in background?", presenterResult.languageDisplayName, shellType) + : localize('runInTerminal.presentationOverride', "Run `{0}` command in `{1}`?", presenterResult.languageDisplayName, shellType); + } + break; + } + } + const confirmationMessages = isFinalAutoApproved ? undefined : { title: confirmationTitle, message: new MarkdownString(args.explanation), @@ -564,11 +615,11 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { const args = invocation.parameters as IRunInTerminalInputParams; this._logService.debug(`RunInTerminalTool: Invoking with options ${JSON.stringify(args)}`); - let toolResultMessage: string | undefined; + let toolResultMessage: string | IMarkdownString | undefined; const chatSessionResource = invocation.context?.sessionResource ?? LocalChatSessionUri.forSession(invocation.context?.sessionId ?? 'no-chat-session'); const chatSessionId = chatSessionResourceToId(chatSessionResource); - const command = toolSpecificData.commandLine.userEdited ?? toolSpecificData.commandLine.toolEdited ?? toolSpecificData.commandLine.original; + let command = toolSpecificData.commandLine.userEdited ?? toolSpecificData.commandLine.toolEdited ?? toolSpecificData.commandLine.original; const didUserEditCommand = ( toolSpecificData.commandLine.userEdited !== undefined && toolSpecificData.commandLine.userEdited !== toolSpecificData.commandLine.original @@ -579,6 +630,12 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { toolSpecificData.commandLine.toolEdited !== toolSpecificData.commandLine.original ); + if (this._sandboxService.isEnabled()) { + await this._sandboxService.getSandboxConfigPath(); + this._logService.info(`RunInTerminalTool: Sandboxing is enabled, wrapping command with srt.`); + command = this._sandboxService.wrapCommand(command); + } + if (token.isCancellationRequested) { throw new CancellationError(); } @@ -722,21 +779,9 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } try { - let strategy: ITerminalExecuteStrategy; - switch (toolTerminal.shellIntegrationQuality) { - case ShellIntegrationQuality.None: { - strategy = this._instantiationService.createInstance(NoneExecuteStrategy, toolTerminal.instance, () => toolTerminal.receivedUserInput ?? false); - toolResultMessage = '$(info) Enable [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) to improve command detection'; - break; - } - case ShellIntegrationQuality.Basic: { - strategy = this._instantiationService.createInstance(BasicExecuteStrategy, toolTerminal.instance, () => toolTerminal.receivedUserInput ?? false, commandDetection!); - break; - } - case ShellIntegrationQuality.Rich: { - strategy = this._instantiationService.createInstance(RichExecuteStrategy, toolTerminal.instance, commandDetection!); - break; - } + const strategy: ITerminalExecuteStrategy = this._getExecuteStrategy(toolTerminal.shellIntegrationQuality, toolTerminal, commandDetection!); + if (toolTerminal.shellIntegrationQuality === ShellIntegrationQuality.None) { + toolResultMessage = '$(info) Enable [shell integration](https://code.visualstudio.com/docs/terminal/shell-integration) to improve command detection'; } this._logService.debug(`RunInTerminalTool: Using \`${strategy.type}\` execute strategy for command \`${command}\``); store.add(strategy.onDidCreateStartMarker(startMarker => { @@ -796,7 +841,6 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } terminalResult = resultArr.join('\n\n'); } - } catch (e) { // Handle timeout case - get output collected so far and return it if (didTimeout && e instanceof CancellationError) { @@ -1040,6 +1084,21 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } } + private _getExecuteStrategy(shellIntegrationQuality: ShellIntegrationQuality, toolTerminal: IToolTerminal, commandDetection: ICommandDetectionCapability): ITerminalExecuteStrategy { + let strategy: ITerminalExecuteStrategy; + switch (shellIntegrationQuality) { + case ShellIntegrationQuality.None: + strategy = this._instantiationService.createInstance(NoneExecuteStrategy, toolTerminal.instance, () => toolTerminal.receivedUserInput ?? false); + break; + case ShellIntegrationQuality.Basic: + strategy = this._instantiationService.createInstance(BasicExecuteStrategy, toolTerminal.instance, () => toolTerminal.receivedUserInput ?? false, commandDetection!); + break; + case ShellIntegrationQuality.Rich: + strategy = this._instantiationService.createInstance(RichExecuteStrategy, toolTerminal.instance, commandDetection!); + break; + } + return strategy; + } // #endregion } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts index 464d8695ce537..b6bea01c0f2f1 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts @@ -9,6 +9,8 @@ import { BugIndicatingError, ErrorNoTelemetry } from '../../../../../base/common import { Lazy } from '../../../../../base/common/lazy.js'; import { Disposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { ITreeSitterLibraryService } from '../../../../../editor/common/services/treeSitter/treeSitterLibraryService.js'; +import { ICommandFileWriteParser } from './commandParsers/commandFileWriteParser.js'; +import { SedFileWriteParser } from './commandParsers/sedFileWriteParser.js'; export const enum TreeSitterCommandParserLanguage { Bash = 'bash', @@ -18,6 +20,9 @@ export const enum TreeSitterCommandParserLanguage { export class TreeSitterCommandParser extends Disposable { private readonly _parser: Lazy>; private readonly _treeCache = this._register(new TreeCache()); + private readonly _commandFileWriteParsers: ICommandFileWriteParser[] = [ + new SedFileWriteParser(), + ]; constructor( @ITreeSitterLibraryService private readonly _treeSitterLibraryService: ITreeSitterLibraryService, @@ -61,6 +66,33 @@ export class TreeSitterCommandParser extends Disposable { return captures.map(e => e.node.text.trim()); } + /** + * Extracts file targets from commands that perform file writes beyond shell redirections. + * Uses registered command parsers (e.g., for `sed -i`) to detect command-specific file writes. + * Returns an array of file paths that would be modified. + */ + async getCommandFileWrites(languageId: TreeSitterCommandParserLanguage, commandLine: string): Promise { + // Currently only bash-like shells are supported for command-specific parsing + if (languageId !== TreeSitterCommandParserLanguage.Bash) { + return []; + } + + // Query for all commands + const query = '(command) @command'; + const captures = await this._queryTree(languageId, commandLine, query); + + const result: string[] = []; + for (const capture of captures) { + const commandText = capture.node.text; + for (const parser of this._commandFileWriteParsers) { + if (parser.canHandle(commandText)) { + result.push(...parser.extractFileWrites(commandText)); + } + } + } + return result; + } + private async _queryTree(languageId: TreeSitterCommandParserLanguage, commandLine: string, querySource: string): Promise { const { tree, query } = await this._doQuery(languageId, commandLine, querySource); return query.captures(tree.rootNode); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index 57351d2b06590..be29b27e9e6ac 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -20,6 +20,10 @@ export const enum TerminalChatAgentToolsSettingId { ShellIntegrationTimeout = 'chat.tools.terminal.shellIntegrationTimeout', AutoReplyToPrompts = 'chat.tools.terminal.autoReplyToPrompts', OutputLocation = 'chat.tools.terminal.outputLocation', + TerminalSandboxEnabled = 'chat.tools.terminal.sandbox.enabled', + TerminalSandboxNetwork = 'chat.tools.terminal.sandbox.network', + TerminalSandboxLinuxFileSystem = 'chat.tools.terminal.sandbox.linuxFileSystem', + TerminalSandboxMacFileSystem = 'chat.tools.terminal.sandbox.macFileSystem', PreventShellHistory = 'chat.tools.terminal.preventShellHistory', EnforceTimeoutFromModel = 'chat.tools.terminal.enforceTimeoutFromModel', @@ -313,15 +317,16 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary('terminalSandboxService'); + +export interface ITerminalSandboxService { + readonly _serviceBrand: undefined; + isEnabled(): boolean; + wrapCommand(command: string): string; + getSandboxConfigPath(forceRefresh?: boolean): Promise; + getTempDir(): URI | undefined; + setNeedsForceUpdateConfigFile(): void; +} + +export class TerminalSandboxService implements ITerminalSandboxService { + readonly _serviceBrand: undefined; + private _srtPath: string; + private _sandboxConfigPath: string | undefined; + private _needsForceUpdateConfigFile = true; + private _tempDir: URI | undefined; + private _sandboxSettingsId: string | undefined; + private _os: OperatingSystem = OS; + + constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IFileService private readonly _fileService: IFileService, + @IEnvironmentService private readonly _environmentService: IEnvironmentService, + @ILogService private readonly _logService: ILogService, + @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, + ) { + const appRoot = dirname(FileAccess.asFileUri('').fsPath); + this._srtPath = join(appRoot, 'node_modules', '.bin', 'srt'); + this._sandboxSettingsId = generateUuid(); + this._initTempDir(); + this._remoteAgentService.getEnvironment().then(remoteEnv => this._os = remoteEnv?.os ?? OS); + } + + public isEnabled(): boolean { + if (this._os === OperatingSystem.Windows) { + return false; + } + return this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxEnabled); + } + + public wrapCommand(command: string): string { + if (!this._sandboxConfigPath || !this._tempDir) { + throw new Error('Sandbox config path or temp dir not initialized'); + } + return `"${this._srtPath}" TMPDIR=${this._tempDir.fsPath} --settings "${this._sandboxConfigPath}" "${command}"`; + } + + public getTempDir(): URI | undefined { + return this._tempDir; + } + + public setNeedsForceUpdateConfigFile(): void { + this._needsForceUpdateConfigFile = true; + } + + public async getSandboxConfigPath(forceRefresh: boolean = false): Promise { + if (!this._sandboxConfigPath || forceRefresh || this._needsForceUpdateConfigFile) { + this._sandboxConfigPath = await this._createSandboxConfig(); + this._needsForceUpdateConfigFile = false; + } + return this._sandboxConfigPath; + } + + private async _createSandboxConfig(): Promise { + + if (this.isEnabled() && !this._tempDir) { + this._initTempDir(); + } + if (this._tempDir) { + const networkSetting = this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxNetwork) ?? {}; + const linuxFileSystemSetting = this._os === OperatingSystem.Linux + ? this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxLinuxFileSystem) ?? {} + : {}; + const macFileSystemSetting = this._os === OperatingSystem.Macintosh + ? this._configurationService.getValue(TerminalChatAgentToolsSettingId.TerminalSandboxMacFileSystem) ?? {} + : {}; + const configFileUri = joinPath(this._tempDir, `vscode-sandbox-settings-${this._sandboxSettingsId}.json`); + const sandboxSettings = { + network: { + allowedDomains: networkSetting.allowedDomains ?? [], + deniedDomains: networkSetting.deniedDomains ?? [] + }, + filesystem: { + denyRead: this._os === OperatingSystem.Macintosh ? macFileSystemSetting.denyRead : linuxFileSystemSetting.denyRead, + allowWrite: this._os === OperatingSystem.Macintosh ? macFileSystemSetting.allowWrite : linuxFileSystemSetting.allowWrite, + denyWrite: this._os === OperatingSystem.Macintosh ? macFileSystemSetting.denyWrite : linuxFileSystemSetting.denyWrite, + } + }; + this._sandboxConfigPath = configFileUri.fsPath; + await this._fileService.createFile(configFileUri, VSBuffer.fromString(JSON.stringify(sandboxSettings, null, '\t')), { overwrite: true }); + return this._sandboxConfigPath; + } + return undefined; + } + + private _initTempDir(): void { + if (this.isEnabled() && isNative) { + this._needsForceUpdateConfigFile = true; + const environmentService = this._environmentService as IEnvironmentService & { tmpDir?: URI }; + this._tempDir = environmentService.tmpDir; + if (!this._tempDir) { + this._logService.warn('TerminalSandboxService: Cannot create sandbox settings file because no tmpDir is available in this environment'); + return; + } + } + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/nodeCommandLinePresenter.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/nodeCommandLinePresenter.test.ts new file mode 100644 index 0000000000000..5beabdd32ce0f --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/nodeCommandLinePresenter.test.ts @@ -0,0 +1,203 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ok, strictEqual } from 'assert'; +import { extractNodeCommand, NodeCommandLinePresenter } from '../../browser/tools/commandLinePresenter/nodeCommandLinePresenter.js'; +import { OperatingSystem } from '../../../../../../base/common/platform.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; + +suite('extractNodeCommand', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('basic extraction', () => { + test('should extract simple node -e command with double quotes', () => { + const result = extractNodeCommand(`node -e "console.log('hello')"`, 'bash', OperatingSystem.Linux); + strictEqual(result, `console.log('hello')`); + }); + + test('should extract nodejs -e command', () => { + const result = extractNodeCommand(`nodejs -e "console.log('hello')"`, 'bash', OperatingSystem.Linux); + strictEqual(result, `console.log('hello')`); + }); + + test('should extract node --eval command', () => { + const result = extractNodeCommand(`node --eval "console.log('hello')"`, 'bash', OperatingSystem.Linux); + strictEqual(result, `console.log('hello')`); + }); + + test('should extract nodejs --eval command', () => { + const result = extractNodeCommand(`nodejs --eval "console.log('hello')"`, 'bash', OperatingSystem.Linux); + strictEqual(result, `console.log('hello')`); + }); + + test('should return undefined for non-node commands', () => { + const result = extractNodeCommand('echo hello', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should return undefined for node without -e flag', () => { + const result = extractNodeCommand('node script.js', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should extract node -e with single quotes', () => { + const result = extractNodeCommand(`node -e 'console.log("hello")'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'console.log("hello")'); + }); + + test('should extract nodejs -e with single quotes', () => { + const result = extractNodeCommand(`nodejs -e 'const x = 1; console.log(x)'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'const x = 1; console.log(x)'); + }); + + test('should extract node --eval with single quotes', () => { + const result = extractNodeCommand(`node --eval 'console.log("hello")'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'console.log("hello")'); + }); + }); + + suite('quote unescaping - Bash', () => { + test('should unescape backslash-escaped quotes in bash', () => { + const result = extractNodeCommand('node -e "console.log(\\"hello\\")"', 'bash', OperatingSystem.Linux); + strictEqual(result, 'console.log("hello")'); + }); + + test('should handle multiple escaped quotes', () => { + const result = extractNodeCommand('node -e "const x = \\"hello\\"; console.log(x)"', 'bash', OperatingSystem.Linux); + strictEqual(result, 'const x = "hello"; console.log(x)'); + }); + }); + + suite('single quotes - literal content', () => { + test('should preserve content literally in single quotes (no unescaping)', () => { + // Single quotes in bash are literal - backslashes are not escape sequences + const result = extractNodeCommand(`node -e 'console.log(\\"hello\\")'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'console.log(\\"hello\\")'); + }); + + test('should handle single quotes in PowerShell', () => { + const result = extractNodeCommand(`node -e 'console.log("hello")'`, 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'console.log("hello")'); + }); + + test('should extract multiline code in single quotes', () => { + const code = `node -e 'for (let i = 0; i < 3; i++) {\n console.log(i);\n}'`; + const result = extractNodeCommand(code, 'bash', OperatingSystem.Linux); + strictEqual(result, `for (let i = 0; i < 3; i++) {\n console.log(i);\n}`); + }); + }); + + suite('quote unescaping - PowerShell', () => { + test('should unescape backtick-escaped quotes in PowerShell', () => { + const result = extractNodeCommand('node -e "console.log(`"hello`")"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'console.log("hello")'); + }); + + test('should handle multiple backtick-escaped quotes', () => { + const result = extractNodeCommand('node -e "const x = `"hello`"; console.log(x)"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'const x = "hello"; console.log(x)'); + }); + + test('should not unescape backslash quotes in PowerShell', () => { + const result = extractNodeCommand('node -e "console.log(\\"hello\\")"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'console.log(\\"hello\\")'); + }); + }); + + suite('multiline code', () => { + test('should extract multiline JavaScript code', () => { + const code = `node -e "for (let i = 0; i < 3; i++) {\n console.log(i);\n}"`; + const result = extractNodeCommand(code, 'bash', OperatingSystem.Linux); + strictEqual(result, `for (let i = 0; i < 3; i++) {\n console.log(i);\n}`); + }); + }); + + suite('edge cases', () => { + test('should handle code with trailing whitespace trimmed', () => { + const result = extractNodeCommand('node -e " console.log(1) "', 'bash', OperatingSystem.Linux); + strictEqual(result, 'console.log(1)'); + }); + + test('should return undefined for empty code', () => { + const result = extractNodeCommand('node -e ""', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should return undefined when quotes are unmatched', () => { + const result = extractNodeCommand('node -e "console.log(1)', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + }); +}); + +suite('NodeCommandLinePresenter', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + const presenter = new NodeCommandLinePresenter(); + + test('should return JavaScript presentation for node -e command', () => { + const result = presenter.present({ + commandLine: `node -e "console.log('hello')"`, + shell: 'bash', + os: OperatingSystem.Linux + }); + ok(result); + strictEqual(result.commandLine, `console.log('hello')`); + strictEqual(result.language, 'javascript'); + strictEqual(result.languageDisplayName, 'Node.js'); + }); + + test('should return JavaScript presentation for nodejs -e command', () => { + const result = presenter.present({ + commandLine: `nodejs -e 'const x = 1; console.log(x)'`, + shell: 'bash', + os: OperatingSystem.Linux + }); + ok(result); + strictEqual(result.commandLine, 'const x = 1; console.log(x)'); + strictEqual(result.language, 'javascript'); + strictEqual(result.languageDisplayName, 'Node.js'); + }); + + test('should return JavaScript presentation for node --eval command', () => { + const result = presenter.present({ + commandLine: `node --eval "console.log('hello')"`, + shell: 'bash', + os: OperatingSystem.Linux + }); + ok(result); + strictEqual(result.commandLine, `console.log('hello')`); + strictEqual(result.language, 'javascript'); + strictEqual(result.languageDisplayName, 'Node.js'); + }); + + test('should return undefined for non-node commands', () => { + const result = presenter.present({ + commandLine: 'echo hello', + shell: 'bash', + os: OperatingSystem.Linux + }); + strictEqual(result, undefined); + }); + + test('should return undefined for regular node script execution', () => { + const result = presenter.present({ + commandLine: 'node script.js', + shell: 'bash', + os: OperatingSystem.Linux + }); + strictEqual(result, undefined); + }); + + test('should handle PowerShell backtick escaping', () => { + const result = presenter.present({ + commandLine: 'node -e "console.log(`"hello`")"', + shell: 'pwsh', + os: OperatingSystem.Windows + }); + ok(result); + strictEqual(result.commandLine, 'console.log("hello")'); + }); +}); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/pythonCommandLinePresenter.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/pythonCommandLinePresenter.test.ts new file mode 100644 index 0000000000000..db8b5e4313e40 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/pythonCommandLinePresenter.test.ts @@ -0,0 +1,176 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ok, strictEqual } from 'assert'; +import { extractPythonCommand, PythonCommandLinePresenter } from '../../browser/tools/commandLinePresenter/pythonCommandLinePresenter.js'; +import { OperatingSystem } from '../../../../../../base/common/platform.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; + +suite('extractPythonCommand', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('basic extraction', () => { + test('should extract simple python -c command with double quotes', () => { + const result = extractPythonCommand('python -c "print(\'hello\')"', 'bash', OperatingSystem.Linux); + strictEqual(result, `print('hello')`); + }); + + test('should extract python3 -c command', () => { + const result = extractPythonCommand('python3 -c "print(\'hello\')"', 'bash', OperatingSystem.Linux); + strictEqual(result, `print('hello')`); + }); + + test('should return undefined for non-python commands', () => { + const result = extractPythonCommand('echo hello', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should return undefined for python without -c flag', () => { + const result = extractPythonCommand('python script.py', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should extract python -c with single quotes', () => { + const result = extractPythonCommand(`python -c 'print("hello")'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'print("hello")'); + }); + + test('should extract python3 -c with single quotes', () => { + const result = extractPythonCommand(`python3 -c 'x = 1; print(x)'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'x = 1; print(x)'); + }); + }); + + suite('quote unescaping - Bash', () => { + test('should unescape backslash-escaped quotes in bash', () => { + const result = extractPythonCommand('python -c "print(\\"hello\\")"', 'bash', OperatingSystem.Linux); + strictEqual(result, 'print("hello")'); + }); + + test('should handle multiple escaped quotes', () => { + const result = extractPythonCommand('python -c "x = \\\"hello\\\"; print(x)"', 'bash', OperatingSystem.Linux); + strictEqual(result, 'x = "hello"; print(x)'); + }); + }); + + suite('single quotes - literal content', () => { + test('should preserve content literally in single quotes (no unescaping)', () => { + // Single quotes in bash are literal - backslashes are not escape sequences + const result = extractPythonCommand(`python -c 'print(\\"hello\\")'`, 'bash', OperatingSystem.Linux); + strictEqual(result, 'print(\\"hello\\")'); + }); + + test('should handle single quotes in PowerShell', () => { + const result = extractPythonCommand(`python -c 'print("hello")'`, 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'print("hello")'); + }); + + test('should extract multiline code in single quotes', () => { + const code = `python -c 'for i in range(3):\n print(i)'`; + const result = extractPythonCommand(code, 'bash', OperatingSystem.Linux); + strictEqual(result, `for i in range(3):\n print(i)`); + }); + }); + + suite('quote unescaping - PowerShell', () => { + test('should unescape backtick-escaped quotes in PowerShell', () => { + const result = extractPythonCommand('python -c "print(`"hello`")"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'print("hello")'); + }); + + test('should handle multiple backtick-escaped quotes', () => { + const result = extractPythonCommand('python -c "x = `"hello`"; print(x)"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'x = "hello"; print(x)'); + }); + + test('should not unescape backslash quotes in PowerShell', () => { + const result = extractPythonCommand('python -c "print(\\"hello\\")"', 'pwsh', OperatingSystem.Windows); + strictEqual(result, 'print(\\"hello\\")'); + }); + }); + + suite('multiline code', () => { + test('should extract multiline python code', () => { + const code = `python -c "for i in range(3):\n print(i)"`; + const result = extractPythonCommand(code, 'bash', OperatingSystem.Linux); + strictEqual(result, `for i in range(3):\n print(i)`); + }); + }); + + suite('edge cases', () => { + test('should handle code with trailing whitespace trimmed', () => { + const result = extractPythonCommand('python -c " print(1) "', 'bash', OperatingSystem.Linux); + strictEqual(result, 'print(1)'); + }); + + test('should return undefined for empty code', () => { + const result = extractPythonCommand('python -c ""', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + + test('should return undefined when quotes are unmatched', () => { + const result = extractPythonCommand('python -c "print(1)', 'bash', OperatingSystem.Linux); + strictEqual(result, undefined); + }); + }); +}); + +suite('PythonCommandLinePresenter', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + const presenter = new PythonCommandLinePresenter(); + + test('should return Python presentation for python -c command', () => { + const result = presenter.present({ + commandLine: `python -c "print('hello')"`, + shell: 'bash', + os: OperatingSystem.Linux + }); + ok(result); + strictEqual(result.commandLine, `print('hello')`); + strictEqual(result.language, 'python'); + strictEqual(result.languageDisplayName, 'Python'); + }); + + test('should return Python presentation for python3 -c command', () => { + const result = presenter.present({ + commandLine: `python3 -c 'x = 1; print(x)'`, + shell: 'bash', + os: OperatingSystem.Linux + }); + ok(result); + strictEqual(result.commandLine, 'x = 1; print(x)'); + strictEqual(result.language, 'python'); + strictEqual(result.languageDisplayName, 'Python'); + }); + + test('should return undefined for non-python commands', () => { + const result = presenter.present({ + commandLine: 'echo hello', + shell: 'bash', + os: OperatingSystem.Linux + }); + strictEqual(result, undefined); + }); + + test('should return undefined for regular python script execution', () => { + const result = presenter.present({ + commandLine: 'python script.py', + shell: 'bash', + os: OperatingSystem.Linux + }); + strictEqual(result, undefined); + }); + + test('should handle PowerShell backtick escaping', () => { + const result = presenter.present({ + commandLine: 'python -c "print(`"hello`")"', + shell: 'pwsh', + os: OperatingSystem.Windows + }); + ok(result); + strictEqual(result.commandLine, 'print("hello")'); + }); +}); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts index f876846529082..0e6ede9ea6fd1 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalHelpers.test.ts @@ -503,3 +503,5 @@ suite('extractCdPrefix', () => { }); }); }); + + diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineAnalyzer/commandLineFileWriteAnalyzer.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineAnalyzer/commandLineFileWriteAnalyzer.test.ts index df69f9be0cfec..89e2102986335 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineAnalyzer/commandLineFileWriteAnalyzer.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineAnalyzer/commandLineFileWriteAnalyzer.test.ts @@ -134,6 +134,40 @@ suite('CommandLineFileWriteAnalyzer', () => { test('error output to /dev/null - allow', () => t('cat missing.txt 2> /dev/null', 'outsideWorkspace', true, 1)); }); + suite('sed in-place editing', () => { + // Basic -i flag variants (inside workspace) + test('sed -i inside workspace - allow', () => t('sed -i \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + test('sed -I (uppercase) inside workspace - allow', () => t('sed -I \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + test('sed --in-place inside workspace - allow', () => t('sed --in-place \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + + // Backup suffix variants (inside workspace) + test('sed -i.bak inside workspace - allow', () => t('sed -i.bak \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + test('sed --in-place=.bak inside workspace - allow', () => t('sed --in-place=.bak \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + test('sed -i with empty backup (macOS) inside workspace - allow', () => t('sed -i \'\' \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + + // Combined flags (inside workspace) + test('sed -ni inside workspace - allow', () => t('sed -ni \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + test('sed -n -i inside workspace - allow', () => t('sed -n -i \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 1)); + + // Multiple files (inside workspace) + test('sed -i multiple files inside workspace - allow', () => t('sed -i \'s/foo/bar/\' file1.txt file2.txt', 'outsideWorkspace', true, 1)); + + // Outside workspace + test('sed -i outside workspace - block', () => t('sed -i \'s/foo/bar/\' /tmp/file.txt', 'outsideWorkspace', false, 1)); + test('sed -i absolute path outside workspace - block', () => t('sed -i \'s/foo/bar/\' /etc/config', 'outsideWorkspace', false, 1)); + test('sed -i mixed inside/outside - block', () => t('sed -i \'s/foo/bar/\' file.txt /tmp/other.txt', 'outsideWorkspace', false, 1)); + + // With blockDetectedFileWrites: all + test('sed -i with all setting - block', () => t('sed -i \'s/foo/bar/\' file.txt', 'all', false, 1)); + + // With blockDetectedFileWrites: never + test('sed -i with never setting - allow', () => t('sed -i \'s/foo/bar/\' file.txt', 'never', true, 1)); + + // Without -i flag (should not detect as file write) + test('sed without -i - no file write detected', () => t('sed \'s/foo/bar/\' file.txt', 'outsideWorkspace', true, 0)); + test('sed with pipe - no file write detected', () => t('cat file.txt | sed \'s/foo/bar/\'', 'outsideWorkspace', true, 0)); + }); + suite('no cwd provided', () => { async function tNoCwd(commandLine: string, blockDetectedFileWrites: 'never' | 'outsideWorkspace' | 'all', expectedAutoApprove: boolean, expectedDisclaimers: number = 0) { configurationService.setUserConfiguration(TerminalChatAgentToolsSettingId.BlockDetectedFileWrites, blockDetectedFileWrites); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts index 25a032e8e2448..a8412b10371ae 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts @@ -32,6 +32,7 @@ import { TestIPCFileSystemProvider } from '../../../../../test/electron-browser/ import { TerminalToolConfirmationStorageKeys } from '../../../../chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js'; import { IChatService, type IChatTerminalToolInvocationData } from '../../../../chat/common/chatService/chatService.js'; import { LocalChatSessionUri } from '../../../../chat/common/model/chatUri.js'; +import { ITerminalSandboxService } from '../../common/terminalSandboxService.js'; import { ILanguageModelToolsService, IPreparedToolInvocation, IToolInvocationPreparationContext, type ToolConfirmationAction } from '../../../../chat/common/tools/languageModelToolsService.js'; import { ITerminalChatService, ITerminalService, type ITerminalInstance } from '../../../../terminal/browser/terminal.js'; import { ITerminalProfileResolverService } from '../../../../terminal/common/terminal.js'; @@ -91,6 +92,14 @@ suite('RunInTerminalTool', () => { instantiationService.stub(IHistoryService, { getLastActiveWorkspaceRoot: () => undefined }); + instantiationService.stub(ITerminalSandboxService, { + _serviceBrand: undefined, + isEnabled: () => false, + wrapCommand: command => command, + getSandboxConfigPath: async () => undefined, + getTempDir: () => undefined, + setNeedsForceUpdateConfigFile: () => { } + }); const treeSitterLibraryService = store.add(instantiationService.createInstance(TreeSitterLibraryService)); treeSitterLibraryService.isTest = true; @@ -353,9 +362,6 @@ suite('RunInTerminalTool', () => { 'find . -fprint output.txt', 'rg --pre cat pattern .', 'rg --hostname-bin hostname pattern .', - 'sed -i "s/foo/bar/g" file.txt', - 'sed -i.bak "s/foo/bar/" file.txt', - 'sed -Ibak "s/foo/bar/" file.txt', 'sed --in-place "s/foo/bar/" file.txt', 'sed -e "s/a/b/" file.txt', 'sed -f script.sed file.txt', @@ -453,7 +459,7 @@ suite('RunInTerminalTool', () => { explanation: 'Start watching for file changes', isBackground: true }); - assertConfirmationRequired(result, 'Run `bash` command? (background terminal)'); + assertConfirmationRequired(result, 'Run `bash` command in background?'); }); test('should auto-approve background commands in allow list', async () => { diff --git a/src/vs/workbench/electron-browser/desktop.contribution.ts b/src/vs/workbench/electron-browser/desktop.contribution.ts index a18ff87a5d8d6..5fad6f9317779 100644 --- a/src/vs/workbench/electron-browser/desktop.contribution.ts +++ b/src/vs/workbench/electron-browser/desktop.contribution.ts @@ -454,10 +454,6 @@ import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../../platform/window/electron-b 'remote-debugging-port': { type: 'string', description: localize('argv.remoteDebuggingPort', "Specifies the port to use for remote debugging.") - }, - 'enable-graphite-invalid-recording-recovery': { - type: 'boolean', - description: localize('argv.enableGraphiteInvalidRecordingRecovery', "Enables recovery from invalid Graphite recordings.") } } }; diff --git a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts index b0da5fe132161..8e35b35ba92d0 100644 --- a/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts +++ b/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts @@ -6,18 +6,20 @@ // version: 1 declare module 'vscode' { - // #region Resource Classes + /** + * Describes a chat resource URI with optional editability. + */ + export type ChatResourceUriDescriptor = + | Uri + | { uri: Uri; isEditable?: boolean }; + /** * Describes a chat resource file. */ export type ChatResourceDescriptor = - | Uri - | { - uri: Uri; - isEditable?: boolean; - } + | ChatResourceUriDescriptor | { id: string; content: string; @@ -71,6 +73,23 @@ declare module 'vscode' { constructor(resource: ChatResourceDescriptor); } + /** + * Represents a skill file resource (SKILL.md) + */ + export class SkillChatResource { + /** + * The skill resource descriptor. + */ + readonly resource: ChatResourceUriDescriptor; + + /** + * Creates a new skill resource from the specified resource URI pointing to SKILL.md. + * The parent folder name needs to match the name of the skill in the frontmatter. + * @param resource The chat resource descriptor. + */ + constructor(resource: ChatResourceUriDescriptor); + } + // #endregion // #region Providers @@ -170,6 +189,41 @@ declare module 'vscode' { // #endregion + // #region SkillProvider + + /** + * Context for querying skills. + */ + export type SkillContext = object; + + /** + * A provider that supplies SKILL.md resources for agents. + */ + export interface SkillProvider { + /** + * A human-readable label for this provider. + */ + readonly label: string; + + /** + * An optional event to signal that skills have changed. + */ + readonly onDidChangeSkills?: Event; + + /** + * Provide the list of skills available. + * @param context Context for the query. + * @param token A cancellation token. + * @returns An array of skill resources or a promise that resolves to such. + */ + provideSkills( + context: SkillContext, + token: CancellationToken + ): ProviderResult; + } + + // #endregion + // #region Chat Provider Registration export namespace chat { @@ -199,6 +253,13 @@ declare module 'vscode' { export function registerPromptFileProvider( provider: PromptFileProvider ): Disposable; + + /** + * Register a provider for skills. + * @param provider The skill provider. + * @returns A disposable that unregisters the provider when disposed. + */ + export function registerSkillProvider(provider: SkillProvider): Disposable; } // #endregion diff --git a/test/mcp/src/automationTools/settings.ts b/test/mcp/src/automationTools/settings.ts index 91502fe1cc907..46f91fe8fbf7a 100644 --- a/test/mcp/src/automationTools/settings.ts +++ b/test/mcp/src/automationTools/settings.ts @@ -37,7 +37,7 @@ export function applySettingsTools(server: McpServer, appService: ApplicationSer 'vscode_automation_settings_add_user_settings', 'Add multiple user settings at once', { - settings: z.array(z.array(z.string()).length(2)).describe('Array of [key, value] setting pairs') + settings: z.array(z.tuple([z.string(), z.string()])).describe('Array of [key, value] setting pairs') }, async (args) => { const { settings } = args;