diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index be8c26eeadd62..8d56465c45a83 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -58,7 +58,7 @@ MANDATORY: Always check for compilation errors before running any tests or valid ### TypeScript compilation steps - If the `#runTasks/getTaskOutput` tool is available, check the `VS Code - Build` watch task output for compilation errors. This task runs `Core - Build` and `Ext - Build` to incrementally compile VS Code TypeScript sources and built-in extensions. Start the task if it's not already running in the background. - If the tool is not available (e.g. in CLI environments) and you only changed code under `src/`, run `npm run compile-check-ts-native` after making changes to type-check the main VS Code sources (it validates `./src/tsconfig.json`). -- If you changed built-in extensions under `extensions/` and the tool is not available, run the corresponding gulp task `gulp compile-extensions` instead so that TypeScript errors in extensions are also reported. +- If you changed built-in extensions under `extensions/` and the tool is not available, run the corresponding gulp task `npm run gulp compile-extensions` instead so that TypeScript errors in extensions are also reported. - For TypeScript changes in the `build` folder, you can simply run `npm run typecheck` in the `build` folder. ### TypeScript validation steps diff --git a/.github/skills/sessions/SKILL.md b/.github/skills/sessions/SKILL.md index d82e03178ffd5..e39957e5f66bc 100644 --- a/.github/skills/sessions/SKILL.md +++ b/.github/skills/sessions/SKILL.md @@ -272,7 +272,9 @@ Views and contributions that should only appear in the agent sessions window (no 1. Run `npm run compile-check-ts-native` to run a repo-wide TypeScript compilation check (including `src/vs/sessions/`). This is a fast way to catch TypeScript errors introduced by your changes. 2. Run `npm run valid-layers-check` to verify layering rules are not violated. -3. Run tests under `src/vs/sessions/test/` to confirm nothing is broken. +3. Use `scripts/test.sh` (or `scripts\test.bat` on Windows) for unit tests (add `--grep ` to filter tests) + +**Important** do not run `tsc` to check for TypeScript errors always use above methods to validate TypeScript changes in `src/vs/**`. ### 10.3 Layout Changes diff --git a/.vscode/extensions.json b/.vscode/extensions.json index bd45eb0e5703c..3fb87652c814d 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,15 +4,11 @@ "recommendations": [ "dbaeumer.vscode-eslint", "editorconfig.editorconfig", + "github.vscode-pull-request-github", "ms-vscode.vscode-github-issue-notebooks", "ms-vscode.extension-test-runner", "jrieken.vscode-pr-pinger", "typescriptteam.native-preview", "ms-vscode.ts-customized-language-service" - ], - "stronglyRecommended": [ - "github.vscode-pull-request-github", - "ms-vscode.vscode-extras", - "ms-vscode.vscode-selfhost-test-provider" ] } diff --git a/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts b/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts index df9abf863ae52..8927b0b7064a9 100644 --- a/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts +++ b/.vscode/extensions/vscode-extras/src/npmUpToDateFeature.ts @@ -197,26 +197,17 @@ export class NpmUpToDateFeature extends vscode.Disposable { return ''; } try { - return this._normalizeFileContent(path.join(this._root, file)); + const script = path.join(this._root, 'build', 'npm', 'installStateHash.ts'); + return cp.execFileSync(process.execPath, [script, '--normalize-file', path.join(this._root, file)], { + cwd: this._root, + timeout: 10_000, + encoding: 'utf8', + }); } catch { return ''; } } - private _normalizeFileContent(filePath: string): string { - const raw = fs.readFileSync(filePath, 'utf8'); - if (path.basename(filePath) === 'package.json') { - const json = JSON.parse(raw); - for (const key of NpmUpToDateFeature._packageJsonIgnoredKeys) { - delete json[key]; - } - return JSON.stringify(json, null, '\t') + '\n'; - } - return raw; - } - - private static readonly _packageJsonIgnoredKeys = ['distro']; - private _getChangedFiles(state: InstallState): { readonly label: string; readonly isFile: boolean }[] { if (!state.saved) { return [{ label: '(no postinstall state found)', isFile: false }]; diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index d3f716f5749cb..40235fad54e5b 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"February 2026\"" + "value": "$MILESTONE=milestone:\"1.111.0\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index b58910ad675e5..50ebbfdb7508d 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"February 2026\"\n\n$MINE=assignee:@me" + "value": "$MILESTONE=milestone:\"1.111.0\"\n\n$MINE=assignee:@me" }, { "kind": 2, diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index b499fd720ff74..ac4ee9cec7d83 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -599,6 +599,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d } result = es.merge(result, gulp.src('resources/win32/VisualElementsManifest.xml', { base: 'resources/win32' }) + .pipe(replace('@@VERSIONFOLDER@@', versionedResourcesFolder ? `${versionedResourcesFolder}\\` : '')) .pipe(rename(product.nameShort + '.VisualElementsManifest.xml'))); result = es.merge(result, gulp.src('.build/policies/win32/**', { base: '.build/policies/win32' }) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 1ea3723af79fa..29a3d47d232b2 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -36,6 +36,7 @@ "--vscode-breadcrumb-focusForeground", "--vscode-breadcrumb-foreground", "--vscode-breadcrumbPicker-background", + "--vscode-browser-border", "--vscode-button-background", "--vscode-button-border", "--vscode-button-foreground", @@ -1009,6 +1010,7 @@ "--text-link-decoration", "--vscode-action-item-auto-timeout", "--monaco-editor-warning-decoration", + "--animation-angle", "--animation-opacity", "--chat-setup-dialog-glow-angle", "--vscode-chat-font-family", diff --git a/build/npm/installStateHash.ts b/build/npm/installStateHash.ts index 1ee80522d6ca1..f52c0a4696d4f 100644 --- a/build/npm/installStateHash.ts +++ b/build/npm/installStateHash.ts @@ -141,11 +141,19 @@ export function readSavedContents(): Record | undefined { // When run directly, output state as JSON for tooling (e.g. the vscode-extras extension). if (import.meta.filename === process.argv[1]) { - console.log(JSON.stringify({ - root, - stateContentsFile, - current: computeState(), - saved: readSavedState(), - files: [...collectInputFiles(), stateFile], - })); + if (process.argv[2] === '--normalize-file') { + const filePath = process.argv[3]; + if (!filePath) { + process.exit(1); + } + process.stdout.write(normalizeFileContent(filePath)); + } else { + console.log(JSON.stringify({ + root, + stateContentsFile, + current: computeState(), + saved: readSavedState(), + files: [...collectInputFiles(), stateFile], + })); + } } diff --git a/extensions/theme-2026/package.json b/extensions/theme-2026/package.json index 305cc066c89f1..8360afdac5ca7 100644 --- a/extensions/theme-2026/package.json +++ b/extensions/theme-2026/package.json @@ -8,9 +8,6 @@ "engines": { "vscode": "^1.85.0" }, - "enabledApiProposals": [ - "css" - ], "categories": [ "Themes" ], @@ -28,11 +25,6 @@ "uiTheme": "vs-dark", "path": "./themes/2026-dark.json" } - ], - "css": [ - { - "path": "./themes/styles.css" - } - ] + ] } } diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 4645a08fa61b4..a346ebba78f22 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -10,7 +10,6 @@ "descriptionForeground": "#8C8C8C", "icon.foreground": "#8C8C8C", "focusBorder": "#3994BCB3", - "contrastBorder": "#333536", "textBlockQuote.background": "#242526", "textBlockQuote.border": "#2A2B2CFF", "textCodeBlock.background": "#242526", @@ -26,7 +25,7 @@ "button.secondaryHoverBackground": "#FFFFFF10", "checkbox.background": "#242526", "checkbox.border": "#333536", - "checkbox.foreground": "#bfbfbf", + "checkbox.foreground": "#8C8C8C", "dropdown.background": "#191A1B", "dropdown.border": "#333536", "dropdown.foreground": "#bfbfbf", @@ -34,7 +33,7 @@ "input.background": "#191A1B", "input.border": "#333536FF", "input.foreground": "#bfbfbf", - "input.placeholderForeground": "#777777", + "input.placeholderForeground": "#555555", "inputOption.activeBackground": "#3994BC33", "inputOption.activeForeground": "#bfbfbf", "inputOption.activeBorder": "#2A2B2CFF", @@ -54,13 +53,14 @@ "badge.background": "#3994BCF0", "badge.foreground": "#FFFFFF", "progressBar.background": "#878889", - "list.activeSelectionBackground": "#262728", + "list.activeSelectionBackground": "#3994BC26", "list.activeSelectionForeground": "#ededed", "list.inactiveSelectionBackground": "#2C2D2E", "list.inactiveSelectionForeground": "#ededed", "list.hoverBackground": "#262728", "list.hoverForeground": "#bfbfbf", "list.dropBackground": "#3994BC1A", + "toolbar.hoverBackground": "#262728", "list.focusBackground": "#3994BC26", "list.focusForeground": "#bfbfbf", "list.focusOutline": "#3994BCB3", @@ -100,12 +100,13 @@ "commandCenter.foreground": "#bfbfbf", "commandCenter.activeForeground": "#bfbfbf", "commandCenter.background": "#191A1B", - "commandCenter.activeBackground": "#252627", + "commandCenter.activeBackground": "#FFFFFF0F", "commandCenter.border": "#2E3031", "editor.background": "#121314", "editor.foreground": "#BBBEBF", "editorStickyScroll.background": "#121314", "editorStickyScrollHover.background": "#202122", + "editorStickyScroll.border": "#2A2B2CFF", "editorLineNumber.foreground": "#858889", "editorLineNumber.activeForeground": "#BBBEBF", "editorCursor.foreground": "#BBBEBF", @@ -190,7 +191,6 @@ "tab.inactiveForeground": "#8C8C8C", "tab.border": "#2A2B2CFF", "tab.lastPinnedBorder": "#2A2B2CFF", - "tab.activeBorder": "#121314", "tab.activeBorderTop": "#3994BC", "tab.hoverBackground": "#262728", "tab.hoverForeground": "#bfbfbf", @@ -199,7 +199,8 @@ "tab.unfocusedInactiveBackground": "#191A1B", "tab.unfocusedInactiveForeground": "#444444", "editorGroupHeader.tabsBackground": "#191A1B", - "editorGroupHeader.tabsBorder": "#2A2B2CFF", + "tab.activeBorder": "#00000000", + "editorGroupHeader.tabsBorder": "#00000000", "breadcrumb.foreground": "#8C8C8C", "breadcrumb.background": "#121314", "breadcrumb.focusForeground": "#bfbfbf", @@ -266,7 +267,8 @@ "charts.yellow": "#E0B97F", "charts.orange": "#CD861A", "charts.green": "#86CF86", - "charts.purple": "#AD80D7" + "charts.purple": "#AD80D7", + "inlineChat.border": "#00000000" }, "tokenColors": [ { diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json index f5a8730719e23..e9db9b3765651 100644 --- a/extensions/theme-2026/themes/2026-light.json +++ b/extensions/theme-2026/themes/2026-light.json @@ -10,7 +10,6 @@ "descriptionForeground": "#606060", "icon.foreground": "#606060", "focusBorder": "#0069CCFF", - "contrastBorder": "#F0F1F2FF", "textBlockQuote.background": "#EAEAEA", "textBlockQuote.border": "#F0F1F2FF", "textCodeBlock.background": "#EAEAEA", @@ -28,7 +27,7 @@ "button.secondaryHoverBackground": "#F2F3F4", "checkbox.background": "#EAEAEA", "checkbox.border": "#D8D8D8", - "checkbox.foreground": "#202020", + "checkbox.foreground": "#606060", "dropdown.background": "#FFFFFF", "dropdown.border": "#D8D8D8", "dropdown.foreground": "#202020", @@ -54,6 +53,7 @@ "widget.border": "#EEEEF1", "editorStickyScroll.shadow": "#00000000", "editorStickyScrollHover.background": "#F0F0F3", + "editorStickyScroll.border": "#F0F1F2FF", "sideBarStickyScroll.shadow": "#00000000", "panelStickyScroll.shadow": "#00000000", "listFilterWidget.shadow": "#00000000", @@ -195,7 +195,6 @@ "tab.inactiveForeground": "#606060", "tab.border": "#F0F1F2FF", "tab.lastPinnedBorder": "#F0F1F2FF", - "tab.activeBorder": "#FAFAFD", "tab.activeBorderTop": "#000000", "tab.hoverBackground": "#DADADA4f", "tab.hoverForeground": "#202020", @@ -204,7 +203,8 @@ "tab.unfocusedInactiveBackground": "#FAFAFD", "tab.unfocusedInactiveForeground": "#BBBBBB", "editorGroupHeader.tabsBackground": "#FAFAFD", - "editorGroupHeader.tabsBorder": "#F0F1F2FF", + "tab.activeBorder": "#00000000", + "editorGroupHeader.tabsBorder": "#00000000", "breadcrumb.foreground": "#606060", "breadcrumb.background": "#FFFFFF", "breadcrumb.focusForeground": "#202020", @@ -272,6 +272,7 @@ "charts.green": "#388A34", "charts.purple": "#652D90", "agentStatusIndicator.background": "#FFFFFF", + "inlineChat.border": "#00000000" }, "tokenColors": [ { diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css deleted file mode 100644 index d76e07491ed4a..0000000000000 --- a/extensions/theme-2026/themes/styles.css +++ /dev/null @@ -1,195 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -:root { - --radius-sm: 4px; - --radius-lg: 8px; -} - -.monaco-pane-view .split-view-view:first-of-type > .pane > .pane-header { - border-top: 1px solid var(--vscode-sideBarSectionHeader-border) !important; -} - -/* Tab border bottom - make transparent */ -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container { - --tabs-border-bottom-color: transparent !important; -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab { - --tab-border-bottom-color: transparent !important; -} - -/* Quick Input (Command Palette) */ - -.monaco-workbench .quick-input-list .quick-input-list-entry .quick-input-list-separator { - height: 16px; - margin-top: 2px; - display: flex; - align-items: center; - font-size: 11px; - padding: 0 4px; - border-radius: var(--vscode-cornerRadius-small) !important; - background: transparent !important; - color: var(--vscode-descriptionForeground) !important; - border: 1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent) !important; - margin-right: 8px; -} - -.monaco-workbench .monaco-list-row.focused .quick-input-list-entry .quick-input-list-separator, -.monaco-workbench .monaco-list-row.selected .quick-input-list-entry .quick-input-list-separator, -.monaco-workbench .monaco-list-row:hover .quick-input-list-entry .quick-input-list-separator { - background: transparent !important; - color: inherit !important; - border: none !important; - padding: 0; -} - -/* Settings */ -.monaco-workbench .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget { - border-radius: var(--radius-sm); - background: transparent !important; - color: var(--vscode-descriptionForeground) !important; - border: 1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent) !important; -} - -/* Input Boxes */ -.monaco-inputbox .monaco-action-bar .action-item .codicon, -.monaco-workbench .search-container .input-box, -.monaco-custom-toggle { - color: var(--vscode-icon-foreground) !important; -} - -/* Chat input toolbar icons should follow icon foreground token */ -.monaco-workbench .interactive-session .chat-input-toolbars .monaco-action-bar .action-item .codicon, -.monaco-workbench .interactive-session .chat-input-toolbars .action-label .codicon { - color: var(--vscode-icon-foreground) !important; -} - -/* Todo List Widget - remove shadows from buttons */ -.monaco-workbench.vs .chat-todo-list-widget .todo-list-expand .monaco-button, -.monaco-workbench.vs .chat-todo-list-widget .todo-list-expand .monaco-button:hover, -.monaco-workbench.vs .chat-todo-list-widget .todo-list-expand .monaco-button:active, -.monaco-workbench.vs .chat-todo-list-widget .todo-clear-button-container .monaco-button, -.monaco-workbench.vs .chat-todo-list-widget .todo-clear-button-container .monaco-button:hover, -.monaco-workbench.vs .chat-todo-list-widget .todo-clear-button-container .monaco-button:active { - box-shadow: none; -} - -/* Link buttons and tool call buttons - remove shadows */ -.monaco-workbench .monaco-button.link-button, -.monaco-workbench .monaco-button.link-button:hover, -.monaco-workbench .monaco-button.link-button:active, -.monaco-workbench .chat-confirmation-widget-title.monaco-button, -.monaco-workbench .chat-confirmation-widget-title.monaco-button:hover, -.monaco-workbench .chat-confirmation-widget-title.monaco-button:active, -.monaco-workbench .chat-used-context-label .monaco-button, -.monaco-workbench .chat-used-context-label .monaco-button:hover, -.monaco-workbench .chat-used-context-label .monaco-button:active { - box-shadow: none; -} - -.monaco-workbench .debug-hover-widget { - color: var(--vscode-editor-foreground) !important; -} - -.monaco-editor .debug-hover-widget .debug-hover-tree .monaco-list-rows .monaco-list-row:hover:not(.highlighted):not(.selected):not(.focused) { - background-color: var(--vscode-list-hoverBackground); -} - -/* Minimap */ - -.monaco-workbench .monaco-editor .minimap canvas { - opacity: 0.85; -} - -.monaco-workbench.vs-dark .monaco-editor .minimap, -.monaco-workbench .monaco-editor .minimap-shadow-visible { - opacity: 0.85; - background-color: var(--vscode-editor-background); - left: 0; -} - -/* Minimap autohide: ensure opacity:0 overrides the 0.85 above */ -.monaco-workbench .monaco-editor .minimap:is(.minimap-autohide-mouseover, .minimap-autohide-scroll) { - opacity: 0; -} - -.monaco-workbench .monaco-editor .minimap:is(.minimap-autohide-mouseover:hover, .minimap-autohide-scroll.active) { - opacity: 0.85; -} - -/* Sticky Scroll */ -.monaco-workbench .monaco-editor .sticky-widget { - border-bottom: var(--vscode-editorWidget-border) !important; - background: transparent !important; -} - -.monaco-workbench .monaco-editor .sticky-widget > * { - background: transparent !important; -} - -.monaco-workbench.vs-dark .monaco-editor .sticky-widget { - border-bottom: none !important; -} - -.monaco-workbench .monaco-editor .sticky-widget .sticky-widget-lines-scrollable { - background: var(--vscode-editor-background) !important; -} - -.monaco-editor .sticky-widget .sticky-line-content { - background: var(--vscode-editor-background) !important; -} - -.monaco-workbench .monaco-editor .sticky-widget .sticky-widget-line-numbers { - background: var(--vscode-editor-background) !important; -} - -.monaco-workbench .monaco-editor .sticky-widget .sticky-line-content:hover { - background: var(--vscode-editorStickyScrollHover-background) !important; -} - -.monaco-editor .rename-box.preview { - border: 1px solid var(--vscode-editorWidget-border); -} - -/* Notebook */ - -.notebookOverlay .monaco-list-row .cell-title-toolbar { - background-color: var(--vscode-editorWidget-background) !important; -} - -/* Inline Chat */ -.monaco-workbench .monaco-editor .inline-chat { - border: none; -} - -/* Command Center */ -.monaco-workbench .part.titlebar .command-center .agent-status-pill { - border-color: var(--vscode-input-border); -} - -.monaco-workbench .part.titlebar .command-center .agent-status-badge { - border-color: var(--vscode-input-border); -} - -.monaco-workbench.vs-dark .monaco-action-bar:not(.vertical) .agent-status-badge-section.sparkle .action-container:hover, -.monaco-workbench.vs-dark .monaco-action-bar:not(.vertical) .agent-status-badge-section.sparkle .dropdown-action-container:hover - { - background-color: var(--vscode-toolbar-hoverBackground); -} - -.monaco-workbench.vs-dark .monaco-action-bar:not(.vertical) .agent-status-badge .monaco-dropdown-with-primary:not(.disabled):hover { - background-color: var(--vscode-commandCenter-activeBackground); -} - -.monaco-workbench .unified-quick-access-tabs { - background: transparent; -} - -/* Quick Input List - use descriptionForeground color for descriptions */ -.monaco-workbench .quick-input-list .monaco-icon-label .label-description { - opacity: 1; - color: var(--vscode-descriptionForeground); -} diff --git a/package-lock.json b/package-lock.json index 9728ddebc18ab..d031af36fdd4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,16 +30,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.167", - "@xterm/addon-image": "^0.10.0-beta.167", - "@xterm/addon-ligatures": "^0.11.0-beta.167", - "@xterm/addon-progress": "^0.3.0-beta.167", - "@xterm/addon-search": "^0.17.0-beta.167", - "@xterm/addon-serialize": "^0.15.0-beta.167", - "@xterm/addon-unicode11": "^0.10.0-beta.167", - "@xterm/addon-webgl": "^0.20.0-beta.166", - "@xterm/headless": "^6.1.0-beta.167", - "@xterm/xterm": "^6.1.0-beta.167", + "@xterm/addon-clipboard": "^0.3.0-beta.168", + "@xterm/addon-image": "^0.10.0-beta.168", + "@xterm/addon-ligatures": "^0.11.0-beta.168", + "@xterm/addon-progress": "^0.3.0-beta.168", + "@xterm/addon-search": "^0.17.0-beta.168", + "@xterm/addon-serialize": "^0.15.0-beta.168", + "@xterm/addon-unicode11": "^0.10.0-beta.168", + "@xterm/addon-webgl": "^0.20.0-beta.167", + "@xterm/headless": "^6.1.0-beta.168", + "@xterm/xterm": "^6.1.0-beta.168", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -1055,29 +1055,6 @@ "node": ">=0.4.0" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, "node_modules/@gulp-sourcemaps/map-sources": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", @@ -3772,30 +3749,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.167.tgz", - "integrity": "sha512-+JSjagAk6okCaGVYFwkKl8qIBfy+W+h7p/qULIi9cC8QyeswOLaE4GOqY5yuGNQYU+zMlrpgR1ttyp0o6y9LHg==", + "version": "0.3.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.168.tgz", + "integrity": "sha512-GIwX30Bto2D0O21Tr8fy9k5MZAscXRab/Y46rWkvVqQp/X3BwJqVpp36uFakOoDdQqjPoZXOsCfJHxnKAP8s/w==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.167.tgz", - "integrity": "sha512-Bxi2oTaX7YM1gup0OSv02n9+tA3P1Ozlu5zyB/ZwSVkepB9FOxCODWD0l3DhWyLGMBqQ+OY/COw5SRxrKyvkNg==", + "version": "0.10.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.168.tgz", + "integrity": "sha512-mGGWeR+xp7aTCHfnc2uQf2CxkRS5JR+5m0nCr5Wqq2FHK28kjfDk/wTeym4YHUqtphqMYzjkNBrvd8z2Yo0mgg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.167.tgz", - "integrity": "sha512-d+9ANnoz6D4J06CjronVolcG+J0jqUWQXbzciRqQkHq0or5k8PYuIj2DuuyBx/0rOaN7JYN347KQ9iylk+++xA==", + "version": "0.11.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.168.tgz", + "integrity": "sha512-vgQgepGSKQwimdXzBIQF2rON2lMCnPMWZHUxNh5VT4FSS9+agAFWR+q46siFagQWlB/ccVZqfkF/M56jr5inAw==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -3805,7 +3782,7 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-ligatures/node_modules/lru-cache": { @@ -3827,63 +3804,63 @@ "license": "ISC" }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.167.tgz", - "integrity": "sha512-8eeaWnp0pnjYaKtOLsXVCE0hTFXS0A2kZCciWp52l6CbNGQsnky4VNWJXKaJrGbS+RHGxT6qWgcB+Mx5ETzZfg==", + "version": "0.3.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.168.tgz", + "integrity": "sha512-TwPp+KUe3TDkA62OujwwAXai6Iy8RnLe3j4BHp350FSJb9W1+1b1e+7qhEmy6J8rjm8SeZfK1ZFKoV4XGaZcDA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.167.tgz", - "integrity": "sha512-1K6POdu0iCdjtW0Bs2z3IGWpMU4gJypbYxGnecbGnsH86rNRGwAKS0bKwWlHAiUQLlOxSGxTiNbazbrDln03FQ==", + "version": "0.17.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.168.tgz", + "integrity": "sha512-C4Z5YoTDKK4pBoXF8UkkWyAAZ4UAAI8L1lZhInDfwfkZ5jGX8LslOpiSG4fKO6h9pcf+sQglyF2IKQEyh8UvmA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.167.tgz", - "integrity": "sha512-7EK/PN7QaUZcNE+bHmt7ELSNK3OBR2UZEuqNkE/0ooha7KqqI9mxZQG53Yn6wYcmRip34OFW9YF49kbTCkFuBg==", + "version": "0.15.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.168.tgz", + "integrity": "sha512-EZ03S0NIm4z8yu2sQZcIoRuvuPv5rSP1lid5tIyOxKN/dJSFSOtM0ErWdDXRv8b4SlqTtv/9DJ7Oo8YMzDxfVw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.167.tgz", - "integrity": "sha512-uOJCfsMhML8GTesUKqCC4CH2cPH9yCIFnixiwgpcE5eLVrLszXW3tny25S/bu6EM+rfvE4nwIvLNTMrQYYnMFA==", + "version": "0.10.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.168.tgz", + "integrity": "sha512-W7XU3pmg/htQAHAYopmlH4i8nVVDyvWvrBVXjSdzwzlwq46bw4owO/IzXIRuwm6YqNOygQPksXHsLDJW04S6dg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.166", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.166.tgz", - "integrity": "sha512-SZmz7HDeSMc4O0++x14ma/UWbK/0Ea8AikHw6V5ex/shjrjwbik7Uf2n8FfG2zMYNgBakvCy/SbwDPtQN+IbRQ==", + "version": "0.20.0-beta.167", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.167.tgz", + "integrity": "sha512-Tiw/weCGGwIN4FNSJ2BGTyer89cpxxubu/LpGv6fiZMUpEo+3am0VwIcL98/3lkxhfr2vcu6Q3YZ5FglPG43Xw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/headless": { - "version": "6.1.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.167.tgz", - "integrity": "sha512-8TokXIwL8UeHhR4mAlUurzrqku5xaDXsikNi0HWpTcPCtZPdntxW36OaHxJmmpuHc8CecdaJehSuhApeW2TuZw==", + "version": "6.1.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.168.tgz", + "integrity": "sha512-9E8teq95/Hxv0r8WLMxfTgqnr1mDFiPpDJH71ZLWyb9TS9jAPQIoJF1h5FqOKx64NBxSQAJUJ7kr4yYbRWeE7g==", "license": "MIT", "workspaces": [ "addons/*" ] }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.167.tgz", - "integrity": "sha512-OOG2gcH9OhEjY+KW3X2s30e1KzaRlynhkF9/oKfb2PNUJBYUdXeww4YAugrz7+nLP8KxCeOdSJrq7VvRzyZrwA==", + "version": "6.1.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.168.tgz", + "integrity": "sha512-emtXKWZmyOZhcEg6StZ3qFU6M++FM506+2V/E//iqMitCDFfJAGJNJYUS5o0/PRN0MaIKo1ladXhfnozAKaGTA==", "license": "MIT", "workspaces": [ "addons/*" @@ -13734,6 +13711,31 @@ "node": ">=0.10.0" } }, + "node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true, + "license": "ISC" + }, "node_modules/prebuild-install": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", @@ -13990,15 +13992,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -14879,13 +14872,13 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", + "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/serve-static": { diff --git a/package.json b/package.json index 91ffe79555c79..7567da0e40ba2 100644 --- a/package.json +++ b/package.json @@ -100,16 +100,16 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.167", - "@xterm/addon-image": "^0.10.0-beta.167", - "@xterm/addon-ligatures": "^0.11.0-beta.167", - "@xterm/addon-progress": "^0.3.0-beta.167", - "@xterm/addon-search": "^0.17.0-beta.167", - "@xterm/addon-serialize": "^0.15.0-beta.167", - "@xterm/addon-unicode11": "^0.10.0-beta.167", - "@xterm/addon-webgl": "^0.20.0-beta.166", - "@xterm/headless": "^6.1.0-beta.167", - "@xterm/xterm": "^6.1.0-beta.167", + "@xterm/addon-clipboard": "^0.3.0-beta.168", + "@xterm/addon-image": "^0.10.0-beta.168", + "@xterm/addon-ligatures": "^0.11.0-beta.168", + "@xterm/addon-progress": "^0.3.0-beta.168", + "@xterm/addon-search": "^0.17.0-beta.168", + "@xterm/addon-serialize": "^0.15.0-beta.168", + "@xterm/addon-unicode11": "^0.10.0-beta.168", + "@xterm/addon-webgl": "^0.20.0-beta.167", + "@xterm/headless": "^6.1.0-beta.168", + "@xterm/xterm": "^6.1.0-beta.168", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", @@ -230,7 +230,8 @@ "node-gyp-build": "4.8.1", "kerberos@2.1.1": { "node-addon-api": "7.1.0" - } + }, + "serialize-javascript": "^7.0.3" }, "repository": { "type": "git", @@ -242,4 +243,4 @@ "optionalDependencies": { "windows-foreground-love": "0.6.1" } -} \ No newline at end of file +} diff --git a/remote/package-lock.json b/remote/package-lock.json index 49b97cb47d00d..bd515187f0d2e 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -22,16 +22,16 @@ "@vscode/vscode-languagedetection": "1.0.23", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.167", - "@xterm/addon-image": "^0.10.0-beta.167", - "@xterm/addon-ligatures": "^0.11.0-beta.167", - "@xterm/addon-progress": "^0.3.0-beta.167", - "@xterm/addon-search": "^0.17.0-beta.167", - "@xterm/addon-serialize": "^0.15.0-beta.167", - "@xterm/addon-unicode11": "^0.10.0-beta.167", - "@xterm/addon-webgl": "^0.20.0-beta.166", - "@xterm/headless": "^6.1.0-beta.167", - "@xterm/xterm": "^6.1.0-beta.167", + "@xterm/addon-clipboard": "^0.3.0-beta.168", + "@xterm/addon-image": "^0.10.0-beta.168", + "@xterm/addon-ligatures": "^0.11.0-beta.168", + "@xterm/addon-progress": "^0.3.0-beta.168", + "@xterm/addon-search": "^0.17.0-beta.168", + "@xterm/addon-serialize": "^0.15.0-beta.168", + "@xterm/addon-unicode11": "^0.10.0-beta.168", + "@xterm/addon-webgl": "^0.20.0-beta.167", + "@xterm/headless": "^6.1.0-beta.168", + "@xterm/xterm": "^6.1.0-beta.168", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -578,30 +578,30 @@ "license": "MIT" }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.167.tgz", - "integrity": "sha512-+JSjagAk6okCaGVYFwkKl8qIBfy+W+h7p/qULIi9cC8QyeswOLaE4GOqY5yuGNQYU+zMlrpgR1ttyp0o6y9LHg==", + "version": "0.3.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.168.tgz", + "integrity": "sha512-GIwX30Bto2D0O21Tr8fy9k5MZAscXRab/Y46rWkvVqQp/X3BwJqVpp36uFakOoDdQqjPoZXOsCfJHxnKAP8s/w==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.167.tgz", - "integrity": "sha512-Bxi2oTaX7YM1gup0OSv02n9+tA3P1Ozlu5zyB/ZwSVkepB9FOxCODWD0l3DhWyLGMBqQ+OY/COw5SRxrKyvkNg==", + "version": "0.10.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.168.tgz", + "integrity": "sha512-mGGWeR+xp7aTCHfnc2uQf2CxkRS5JR+5m0nCr5Wqq2FHK28kjfDk/wTeym4YHUqtphqMYzjkNBrvd8z2Yo0mgg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.167.tgz", - "integrity": "sha512-d+9ANnoz6D4J06CjronVolcG+J0jqUWQXbzciRqQkHq0or5k8PYuIj2DuuyBx/0rOaN7JYN347KQ9iylk+++xA==", + "version": "0.11.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.168.tgz", + "integrity": "sha512-vgQgepGSKQwimdXzBIQF2rON2lMCnPMWZHUxNh5VT4FSS9+agAFWR+q46siFagQWlB/ccVZqfkF/M56jr5inAw==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -611,67 +611,67 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.167.tgz", - "integrity": "sha512-8eeaWnp0pnjYaKtOLsXVCE0hTFXS0A2kZCciWp52l6CbNGQsnky4VNWJXKaJrGbS+RHGxT6qWgcB+Mx5ETzZfg==", + "version": "0.3.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.168.tgz", + "integrity": "sha512-TwPp+KUe3TDkA62OujwwAXai6Iy8RnLe3j4BHp350FSJb9W1+1b1e+7qhEmy6J8rjm8SeZfK1ZFKoV4XGaZcDA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.167.tgz", - "integrity": "sha512-1K6POdu0iCdjtW0Bs2z3IGWpMU4gJypbYxGnecbGnsH86rNRGwAKS0bKwWlHAiUQLlOxSGxTiNbazbrDln03FQ==", + "version": "0.17.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.168.tgz", + "integrity": "sha512-C4Z5YoTDKK4pBoXF8UkkWyAAZ4UAAI8L1lZhInDfwfkZ5jGX8LslOpiSG4fKO6h9pcf+sQglyF2IKQEyh8UvmA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.167.tgz", - "integrity": "sha512-7EK/PN7QaUZcNE+bHmt7ELSNK3OBR2UZEuqNkE/0ooha7KqqI9mxZQG53Yn6wYcmRip34OFW9YF49kbTCkFuBg==", + "version": "0.15.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.168.tgz", + "integrity": "sha512-EZ03S0NIm4z8yu2sQZcIoRuvuPv5rSP1lid5tIyOxKN/dJSFSOtM0ErWdDXRv8b4SlqTtv/9DJ7Oo8YMzDxfVw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.167.tgz", - "integrity": "sha512-uOJCfsMhML8GTesUKqCC4CH2cPH9yCIFnixiwgpcE5eLVrLszXW3tny25S/bu6EM+rfvE4nwIvLNTMrQYYnMFA==", + "version": "0.10.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.168.tgz", + "integrity": "sha512-W7XU3pmg/htQAHAYopmlH4i8nVVDyvWvrBVXjSdzwzlwq46bw4owO/IzXIRuwm6YqNOygQPksXHsLDJW04S6dg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.166", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.166.tgz", - "integrity": "sha512-SZmz7HDeSMc4O0++x14ma/UWbK/0Ea8AikHw6V5ex/shjrjwbik7Uf2n8FfG2zMYNgBakvCy/SbwDPtQN+IbRQ==", + "version": "0.20.0-beta.167", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.167.tgz", + "integrity": "sha512-Tiw/weCGGwIN4FNSJ2BGTyer89cpxxubu/LpGv6fiZMUpEo+3am0VwIcL98/3lkxhfr2vcu6Q3YZ5FglPG43Xw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/headless": { - "version": "6.1.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.167.tgz", - "integrity": "sha512-8TokXIwL8UeHhR4mAlUurzrqku5xaDXsikNi0HWpTcPCtZPdntxW36OaHxJmmpuHc8CecdaJehSuhApeW2TuZw==", + "version": "6.1.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.168.tgz", + "integrity": "sha512-9E8teq95/Hxv0r8WLMxfTgqnr1mDFiPpDJH71ZLWyb9TS9jAPQIoJF1h5FqOKx64NBxSQAJUJ7kr4yYbRWeE7g==", "license": "MIT", "workspaces": [ "addons/*" ] }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.167.tgz", - "integrity": "sha512-OOG2gcH9OhEjY+KW3X2s30e1KzaRlynhkF9/oKfb2PNUJBYUdXeww4YAugrz7+nLP8KxCeOdSJrq7VvRzyZrwA==", + "version": "6.1.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.168.tgz", + "integrity": "sha512-emtXKWZmyOZhcEg6StZ3qFU6M++FM506+2V/E//iqMitCDFfJAGJNJYUS5o0/PRN0MaIKo1ladXhfnozAKaGTA==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/remote/package.json b/remote/package.json index 2de8217e16d43..cef1d91c5caf7 100644 --- a/remote/package.json +++ b/remote/package.json @@ -17,16 +17,16 @@ "@vscode/vscode-languagedetection": "1.0.23", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.2.0", - "@xterm/addon-clipboard": "^0.3.0-beta.167", - "@xterm/addon-image": "^0.10.0-beta.167", - "@xterm/addon-ligatures": "^0.11.0-beta.167", - "@xterm/addon-progress": "^0.3.0-beta.167", - "@xterm/addon-search": "^0.17.0-beta.167", - "@xterm/addon-serialize": "^0.15.0-beta.167", - "@xterm/addon-unicode11": "^0.10.0-beta.167", - "@xterm/addon-webgl": "^0.20.0-beta.166", - "@xterm/headless": "^6.1.0-beta.167", - "@xterm/xterm": "^6.1.0-beta.167", + "@xterm/addon-clipboard": "^0.3.0-beta.168", + "@xterm/addon-image": "^0.10.0-beta.168", + "@xterm/addon-ligatures": "^0.11.0-beta.168", + "@xterm/addon-progress": "^0.3.0-beta.168", + "@xterm/addon-search": "^0.17.0-beta.168", + "@xterm/addon-serialize": "^0.15.0-beta.168", + "@xterm/addon-unicode11": "^0.10.0-beta.168", + "@xterm/addon-webgl": "^0.20.0-beta.167", + "@xterm/headless": "^6.1.0-beta.168", + "@xterm/xterm": "^6.1.0-beta.168", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 0cd6aee9fb79f..0452b9cff95c2 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -14,15 +14,15 @@ "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.23", - "@xterm/addon-clipboard": "^0.3.0-beta.167", - "@xterm/addon-image": "^0.10.0-beta.167", - "@xterm/addon-ligatures": "^0.11.0-beta.167", - "@xterm/addon-progress": "^0.3.0-beta.167", - "@xterm/addon-search": "^0.17.0-beta.167", - "@xterm/addon-serialize": "^0.15.0-beta.167", - "@xterm/addon-unicode11": "^0.10.0-beta.167", - "@xterm/addon-webgl": "^0.20.0-beta.166", - "@xterm/xterm": "^6.1.0-beta.167", + "@xterm/addon-clipboard": "^0.3.0-beta.168", + "@xterm/addon-image": "^0.10.0-beta.168", + "@xterm/addon-ligatures": "^0.11.0-beta.168", + "@xterm/addon-progress": "^0.3.0-beta.168", + "@xterm/addon-search": "^0.17.0-beta.168", + "@xterm/addon-serialize": "^0.15.0-beta.168", + "@xterm/addon-unicode11": "^0.10.0-beta.168", + "@xterm/addon-webgl": "^0.20.0-beta.167", + "@xterm/xterm": "^6.1.0-beta.168", "jschardet": "3.1.4", "katex": "^0.16.22", "tas-client": "0.3.1", @@ -100,30 +100,30 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.3.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.167.tgz", - "integrity": "sha512-+JSjagAk6okCaGVYFwkKl8qIBfy+W+h7p/qULIi9cC8QyeswOLaE4GOqY5yuGNQYU+zMlrpgR1ttyp0o6y9LHg==", + "version": "0.3.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.168.tgz", + "integrity": "sha512-GIwX30Bto2D0O21Tr8fy9k5MZAscXRab/Y46rWkvVqQp/X3BwJqVpp36uFakOoDdQqjPoZXOsCfJHxnKAP8s/w==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-image": { - "version": "0.10.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.167.tgz", - "integrity": "sha512-Bxi2oTaX7YM1gup0OSv02n9+tA3P1Ozlu5zyB/ZwSVkepB9FOxCODWD0l3DhWyLGMBqQ+OY/COw5SRxrKyvkNg==", + "version": "0.10.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.168.tgz", + "integrity": "sha512-mGGWeR+xp7aTCHfnc2uQf2CxkRS5JR+5m0nCr5Wqq2FHK28kjfDk/wTeym4YHUqtphqMYzjkNBrvd8z2Yo0mgg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.11.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.167.tgz", - "integrity": "sha512-d+9ANnoz6D4J06CjronVolcG+J0jqUWQXbzciRqQkHq0or5k8PYuIj2DuuyBx/0rOaN7JYN347KQ9iylk+++xA==", + "version": "0.11.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.168.tgz", + "integrity": "sha512-vgQgepGSKQwimdXzBIQF2rON2lMCnPMWZHUxNh5VT4FSS9+agAFWR+q46siFagQWlB/ccVZqfkF/M56jr5inAw==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -133,58 +133,58 @@ "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-progress": { - "version": "0.3.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.167.tgz", - "integrity": "sha512-8eeaWnp0pnjYaKtOLsXVCE0hTFXS0A2kZCciWp52l6CbNGQsnky4VNWJXKaJrGbS+RHGxT6qWgcB+Mx5ETzZfg==", + "version": "0.3.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.168.tgz", + "integrity": "sha512-TwPp+KUe3TDkA62OujwwAXai6Iy8RnLe3j4BHp350FSJb9W1+1b1e+7qhEmy6J8rjm8SeZfK1ZFKoV4XGaZcDA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-search": { - "version": "0.17.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.167.tgz", - "integrity": "sha512-1K6POdu0iCdjtW0Bs2z3IGWpMU4gJypbYxGnecbGnsH86rNRGwAKS0bKwWlHAiUQLlOxSGxTiNbazbrDln03FQ==", + "version": "0.17.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.168.tgz", + "integrity": "sha512-C4Z5YoTDKK4pBoXF8UkkWyAAZ4UAAI8L1lZhInDfwfkZ5jGX8LslOpiSG4fKO6h9pcf+sQglyF2IKQEyh8UvmA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.15.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.167.tgz", - "integrity": "sha512-7EK/PN7QaUZcNE+bHmt7ELSNK3OBR2UZEuqNkE/0ooha7KqqI9mxZQG53Yn6wYcmRip34OFW9YF49kbTCkFuBg==", + "version": "0.15.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.168.tgz", + "integrity": "sha512-EZ03S0NIm4z8yu2sQZcIoRuvuPv5rSP1lid5tIyOxKN/dJSFSOtM0ErWdDXRv8b4SlqTtv/9DJ7Oo8YMzDxfVw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.10.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.167.tgz", - "integrity": "sha512-uOJCfsMhML8GTesUKqCC4CH2cPH9yCIFnixiwgpcE5eLVrLszXW3tny25S/bu6EM+rfvE4nwIvLNTMrQYYnMFA==", + "version": "0.10.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.168.tgz", + "integrity": "sha512-W7XU3pmg/htQAHAYopmlH4i8nVVDyvWvrBVXjSdzwzlwq46bw4owO/IzXIRuwm6YqNOygQPksXHsLDJW04S6dg==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.20.0-beta.166", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.166.tgz", - "integrity": "sha512-SZmz7HDeSMc4O0++x14ma/UWbK/0Ea8AikHw6V5ex/shjrjwbik7Uf2n8FfG2zMYNgBakvCy/SbwDPtQN+IbRQ==", + "version": "0.20.0-beta.167", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.167.tgz", + "integrity": "sha512-Tiw/weCGGwIN4FNSJ2BGTyer89cpxxubu/LpGv6fiZMUpEo+3am0VwIcL98/3lkxhfr2vcu6Q3YZ5FglPG43Xw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^6.1.0-beta.167" + "@xterm/xterm": "^6.1.0-beta.168" } }, "node_modules/@xterm/xterm": { - "version": "6.1.0-beta.167", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.167.tgz", - "integrity": "sha512-OOG2gcH9OhEjY+KW3X2s30e1KzaRlynhkF9/oKfb2PNUJBYUdXeww4YAugrz7+nLP8KxCeOdSJrq7VvRzyZrwA==", + "version": "6.1.0-beta.168", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.168.tgz", + "integrity": "sha512-emtXKWZmyOZhcEg6StZ3qFU6M++FM506+2V/E//iqMitCDFfJAGJNJYUS5o0/PRN0MaIKo1ladXhfnozAKaGTA==", "license": "MIT", "workspaces": [ "addons/*" diff --git a/remote/web/package.json b/remote/web/package.json index a641d6346c079..d91fc919490b4 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -9,15 +9,15 @@ "@vscode/iconv-lite-umd": "0.7.1", "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.23", - "@xterm/addon-clipboard": "^0.3.0-beta.167", - "@xterm/addon-image": "^0.10.0-beta.167", - "@xterm/addon-ligatures": "^0.11.0-beta.167", - "@xterm/addon-progress": "^0.3.0-beta.167", - "@xterm/addon-search": "^0.17.0-beta.167", - "@xterm/addon-serialize": "^0.15.0-beta.167", - "@xterm/addon-unicode11": "^0.10.0-beta.167", - "@xterm/addon-webgl": "^0.20.0-beta.166", - "@xterm/xterm": "^6.1.0-beta.167", + "@xterm/addon-clipboard": "^0.3.0-beta.168", + "@xterm/addon-image": "^0.10.0-beta.168", + "@xterm/addon-ligatures": "^0.11.0-beta.168", + "@xterm/addon-progress": "^0.3.0-beta.168", + "@xterm/addon-search": "^0.17.0-beta.168", + "@xterm/addon-serialize": "^0.15.0-beta.168", + "@xterm/addon-unicode11": "^0.10.0-beta.168", + "@xterm/addon-webgl": "^0.20.0-beta.167", + "@xterm/xterm": "^6.1.0-beta.168", "jschardet": "3.1.4", "katex": "^0.16.22", "tas-client": "0.3.1", diff --git a/resources/win32/VisualElementsManifest.xml b/resources/win32/VisualElementsManifest.xml index 40efd0a396e87..5ad1283cd7e10 100644 --- a/resources/win32/VisualElementsManifest.xml +++ b/resources/win32/VisualElementsManifest.xml @@ -2,8 +2,8 @@ diff --git a/src/vs/base/browser/ui/actionbar/actionbar.css b/src/vs/base/browser/ui/actionbar/actionbar.css index 467b1ff6efa32..e9e55ad9b269c 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.css +++ b/src/vs/base/browser/ui/actionbar/actionbar.css @@ -50,7 +50,7 @@ display: flex; font-size: 11px; padding: 3px; - border-radius: 5px; + border-radius: var(--vscode-cornerRadius-medium); } .monaco-action-bar .action-item.disabled .action-label:not(.icon) , diff --git a/src/vs/base/browser/ui/inputbox/inputBox.css b/src/vs/base/browser/ui/inputbox/inputBox.css index 827a19f29b487..dc5e637f6ee56 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.css +++ b/src/vs/base/browser/ui/inputbox/inputBox.css @@ -103,4 +103,5 @@ background-repeat: no-repeat; width: 16px; height: 16px; + color: var(--vscode-icon-foreground); } diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.css b/src/vs/editor/browser/viewParts/minimap/minimap.css index 73814f2397990..e6ced7d3dc851 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.css +++ b/src/vs/editor/browser/viewParts/minimap/minimap.css @@ -61,3 +61,7 @@ .monaco-editor .minimap { z-index: 5; } + +.monaco-editor .minimap canvas { + opacity: 0.9; +} diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index f6d3dc6133ffc..74c89c2c486a9 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -119,7 +119,7 @@ .action-widget .monaco-list-row.action { display: flex; - gap: 6px; + gap: 8px; align-items: center; } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index f7168ac83af66..e563293ea2a4b 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -255,6 +255,7 @@ export class MenuId { static readonly ChatExecute = new MenuId('ChatExecute'); static readonly ChatExecuteQueue = new MenuId('ChatExecuteQueue'); static readonly ChatInput = new MenuId('ChatInput'); + static readonly ChatInputSecondary = new MenuId('ChatInputSecondary'); static readonly ChatInputSide = new MenuId('ChatInputSide'); static readonly ChatModePicker = new MenuId('ChatModePicker'); static readonly ChatEditingWidgetToolbar = new MenuId('ChatEditingWidgetToolbar'); diff --git a/src/vs/platform/browserView/common/browserView.ts b/src/vs/platform/browserView/common/browserView.ts index 499b0ef5cb929..ec51dec6b8f04 100644 --- a/src/vs/platform/browserView/common/browserView.ts +++ b/src/vs/platform/browserView/common/browserView.ts @@ -14,6 +14,7 @@ export interface IBrowserViewBounds { width: number; height: number; zoomFactor: number; + cornerRadius: number; } export interface IBrowserViewCaptureScreenshotOptions { diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index 11f6f39b59266..10fb6b62afaff 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -385,6 +385,7 @@ export class BrowserView extends Disposable implements ICDPTarget { } this._view.webContents.setZoomFactor(bounds.zoomFactor); + this._view.setBorderRadius(Math.round(bounds.cornerRadius * bounds.zoomFactor)); this._view.setBounds({ x: Math.round(bounds.x * bounds.zoomFactor), y: Math.round(bounds.y * bounds.zoomFactor), diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index 925b82a8a28f8..fc73e57f82433 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -5,7 +5,6 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { Event } from '../../../base/common/event.js'; -import { DisposableStore } from '../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js'; import { basename } from '../../../base/common/resources.js'; @@ -287,23 +286,12 @@ export const IDialogService = createDecorator('dialogService'); export interface ICustomDialogOptions { readonly buttonDetails?: string[]; - readonly buttonOptions?: Array; readonly markdownDetails?: ICustomDialogMarkdown[]; - readonly renderBody?: (container: HTMLElement, disposables: DisposableStore) => void; readonly classes?: string[]; readonly icon?: ThemeIcon; readonly disableCloseAction?: boolean; } -export interface ICustomDialogButtonOptions { - readonly sublabel?: string; - readonly styleButton?: (button: ICustomDialogButtonControl) => void; -} - -export interface ICustomDialogButtonControl { - set enabled(value: boolean); -} - export interface ICustomDialogMarkdown { readonly markdown: IMarkdownString; readonly classes?: string[]; diff --git a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts b/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts index 00b2a6ce99357..fe258ed580c87 100644 --- a/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts +++ b/src/vs/platform/extensionRecommendations/common/extensionRecommendations.ts @@ -45,7 +45,5 @@ export interface IExtensionRecommendationNotificationService { promptImportantExtensionsInstallNotification(recommendations: IExtensionRecommendations): Promise; promptWorkspaceRecommendations(recommendations: Array): Promise; - promptStronglyRecommendedExtensions(recommendations: Array): Promise; - resetStronglyRecommendedIgnoreState(): void; } diff --git a/src/vs/platform/extensionRecommendations/common/extensionRecommendationsIpc.ts b/src/vs/platform/extensionRecommendations/common/extensionRecommendationsIpc.ts index 28ae56a38bed0..da863e3c6e29a 100644 --- a/src/vs/platform/extensionRecommendations/common/extensionRecommendationsIpc.ts +++ b/src/vs/platform/extensionRecommendations/common/extensionRecommendationsIpc.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from '../../../base/common/event.js'; -import { URI } from '../../../base/common/uri.js'; import { IChannel, IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; import { IExtensionRecommendationNotificationService, IExtensionRecommendations, RecommendationsNotificationResult } from './extensionRecommendations.js'; @@ -24,18 +23,10 @@ export class ExtensionRecommendationNotificationServiceChannelClient implements throw new Error('not supported'); } - promptStronglyRecommendedExtensions(recommendations: Array): Promise { - throw new Error('not supported'); - } - hasToIgnoreRecommendationNotifications(): boolean { throw new Error('not supported'); } - resetStronglyRecommendedIgnoreState(): void { - throw new Error('not supported'); - } - } export class ExtensionRecommendationNotificationServiceChannel implements IServerChannel { diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index fb796867b8bc2..a9bc2d2fa1092 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -60,7 +60,7 @@ const _allApiProposals = { }, chatParticipantPrivate: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts', - version: 14 + version: 15 }, chatPromptFiles: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts', diff --git a/src/vs/platform/hover/browser/hoverService.ts b/src/vs/platform/hover/browser/hoverService.ts index 6779e7e1c5e79..cfb53e2e686ae 100644 --- a/src/vs/platform/hover/browser/hoverService.ts +++ b/src/vs/platform/hover/browser/hoverService.ts @@ -248,6 +248,7 @@ export class HoverService extends Disposable implements IHoverService { } private _createHover(options: IHoverOptions, skipLastFocusedUpdate?: boolean): ICreateHoverResult | undefined { + this._currentDelayedHover?.dispose(); this._currentDelayedHover = undefined; if (options.content === '') { diff --git a/src/vs/platform/mcp/common/mcpManagement.ts b/src/vs/platform/mcp/common/mcpManagement.ts index 9c2b7e73d9023..834068a98b6c7 100644 --- a/src/vs/platform/mcp/common/mcpManagement.ts +++ b/src/vs/platform/mcp/common/mcpManagement.ts @@ -10,13 +10,14 @@ import { IIterativePager } from '../../../base/common/paging.js'; import { URI } from '../../../base/common/uri.js'; import { SortBy, SortOrder } from '../../extensionManagement/common/extensionManagement.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; -import { IMcpServerConfiguration, IMcpServerVariable } from './mcpPlatformTypes.js'; +import { IMcpSandboxConfiguration, IMcpServerConfiguration, IMcpServerVariable } from './mcpPlatformTypes.js'; export type InstallSource = 'gallery' | 'local'; export interface ILocalMcpServer { readonly name: string; readonly config: IMcpServerConfiguration; + readonly rootSandbox?: IMcpSandboxConfiguration; readonly version?: string; readonly mcpResource: URI; readonly location?: URI; diff --git a/src/vs/platform/mcp/common/mcpManagementService.ts b/src/vs/platform/mcp/common/mcpManagementService.ts index 5f72d29a8fe03..ec10b0f0ea947 100644 --- a/src/vs/platform/mcp/common/mcpManagementService.ts +++ b/src/vs/platform/mcp/common/mcpManagementService.ts @@ -22,7 +22,7 @@ import { ILogService } from '../../log/common/log.js'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; import { DidUninstallMcpServerEvent, IGalleryMcpServer, ILocalMcpServer, IMcpGalleryService, IMcpManagementService, IMcpServerInput, IGalleryMcpServerConfiguration, InstallMcpServerEvent, InstallMcpServerResult, RegistryType, UninstallMcpServerEvent, InstallOptions, UninstallOptions, IInstallableMcpServer, IAllowedMcpServersService, IMcpServerArgument, IMcpServerKeyValueInput, McpServerConfigurationParseResult } from './mcpManagement.js'; -import { IMcpServerVariable, McpServerVariableType, IMcpServerConfiguration, McpServerType } from './mcpPlatformTypes.js'; +import { IMcpSandboxConfiguration, IMcpServerVariable, McpServerVariableType, IMcpServerConfiguration, McpServerType } from './mcpPlatformTypes.js'; import { IMcpResourceScannerService, McpResourceTarget } from './mcpResourceScannerService.js'; export interface ILocalMcpServerInfo { @@ -358,7 +358,7 @@ export abstract class AbstractMcpResourceManagementService extends AbstractCommo const scannedMcpServers = await this.mcpResourceScannerService.scanMcpServers(this.mcpResource, this.target); if (scannedMcpServers.servers) { await Promise.allSettled(Object.entries(scannedMcpServers.servers).map(async ([name, scannedServer]) => { - const server = await this.scanLocalServer(name, scannedServer); + const server = await this.scanLocalServer(name, scannedServer, scannedMcpServers.sandbox); local.set(name, server); })); } @@ -426,7 +426,7 @@ export abstract class AbstractMcpResourceManagementService extends AbstractCommo return Array.from(this.local.values()); } - protected async scanLocalServer(name: string, config: IMcpServerConfiguration): Promise { + protected async scanLocalServer(name: string, config: IMcpServerConfiguration, rootSandbox?: IMcpSandboxConfiguration): Promise { let mcpServerInfo = await this.getLocalServerInfo(name, config); if (!mcpServerInfo) { mcpServerInfo = { name, version: config.version, galleryUrl: isString(config.gallery) ? config.gallery : undefined }; @@ -435,6 +435,7 @@ export abstract class AbstractMcpResourceManagementService extends AbstractCommo return { name, config, + rootSandbox, mcpResource: this.mcpResource, version: mcpServerInfo.version, location: mcpServerInfo.location, diff --git a/src/vs/platform/mcp/common/mcpPlatformTypes.ts b/src/vs/platform/mcp/common/mcpPlatformTypes.ts index 985d17f1dc7ac..dc4fb38172e7c 100644 --- a/src/vs/platform/mcp/common/mcpPlatformTypes.ts +++ b/src/vs/platform/mcp/common/mcpPlatformTypes.ts @@ -58,7 +58,6 @@ export interface IMcpStdioServerConfiguration extends ICommonMcpServerConfigurat readonly envFile?: string; readonly cwd?: string; readonly sandboxEnabled?: boolean; - readonly sandbox?: IMcpSandboxConfiguration; readonly dev?: IMcpDevModeConfig; } diff --git a/src/vs/platform/mcp/common/mcpResourceScannerService.ts b/src/vs/platform/mcp/common/mcpResourceScannerService.ts index e0c67bb8b83e0..151238228b5e3 100644 --- a/src/vs/platform/mcp/common/mcpResourceScannerService.ts +++ b/src/vs/platform/mcp/common/mcpResourceScannerService.ts @@ -188,7 +188,7 @@ export class McpResourceScannerService extends Disposable implements IMcpResourc if (servers.length > 0) { userMcpServers.servers = {}; for (const [serverName, server] of servers) { - userMcpServers.servers[serverName] = this.sanitizeServer(server, scannedMcpServers.sandbox); + userMcpServers.servers[serverName] = this.sanitizeServer(server); } } return userMcpServers; @@ -203,14 +203,14 @@ export class McpResourceScannerService extends Disposable implements IMcpResourc if (servers.length > 0) { scannedMcpServers.servers = {}; for (const [serverName, config] of servers) { - const serverConfig = this.sanitizeServer(config, scannedMcpServers.sandbox); + const serverConfig = this.sanitizeServer(config); scannedMcpServers.servers[serverName] = serverConfig; } } return scannedMcpServers; } - private sanitizeServer(serverOrConfig: IOldScannedMcpServer | Mutable, sandbox?: IMcpSandboxConfiguration): IMcpServerConfiguration { + private sanitizeServer(serverOrConfig: IOldScannedMcpServer | Mutable): IMcpServerConfiguration { let server: IMcpServerConfiguration; if ((serverOrConfig).config) { const oldScannedMcpServer = serverOrConfig; @@ -226,11 +226,6 @@ export class McpResourceScannerService extends Disposable implements IMcpResourc if (server.type === undefined || (server.type !== McpServerType.REMOTE && server.type !== McpServerType.LOCAL)) { (>server).type = (server).command ? McpServerType.LOCAL : McpServerType.REMOTE; } - - if (sandbox && server.type === McpServerType.LOCAL) { - (>server).sandbox = sandbox; - } - return server; } diff --git a/src/vs/platform/mcp/test/common/mcpManagementService.test.ts b/src/vs/platform/mcp/test/common/mcpManagementService.test.ts index 78ee329965290..7245ffd376bdd 100644 --- a/src/vs/platform/mcp/test/common/mcpManagementService.test.ts +++ b/src/vs/platform/mcp/test/common/mcpManagementService.test.ts @@ -4,14 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { VSBuffer } from '../../../../base/common/buffer.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { AbstractCommonMcpManagementService } from '../../common/mcpManagementService.js'; -import { IGalleryMcpServer, IGalleryMcpServerConfiguration, IInstallableMcpServer, ILocalMcpServer, InstallOptions, RegistryType, TransportType, UninstallOptions } from '../../common/mcpManagement.js'; -import { McpServerType, McpServerVariableType, IMcpServerVariable } from '../../common/mcpPlatformTypes.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../base/common/network.js'; +import { AbstractCommonMcpManagementService, AbstractMcpResourceManagementService } from '../../common/mcpManagementService.js'; +import { IGalleryMcpServer, IGalleryMcpServerConfiguration, IInstallableMcpServer, ILocalMcpServer, IMcpGalleryService, InstallOptions, RegistryType, TransportType, UninstallOptions } from '../../common/mcpManagement.js'; +import { IMcpSandboxConfiguration, McpServerType, McpServerVariableType, IMcpServerConfiguration, IMcpServerVariable } from '../../common/mcpPlatformTypes.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { Event } from '../../../../base/common/event.js'; import { URI } from '../../../../base/common/uri.js'; +import { ConfigurationTarget } from '../../../configuration/common/configuration.js'; +import { FileService } from '../../../files/common/fileService.js'; +import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js'; import { NullLogService } from '../../../log/common/log.js'; +import { McpResourceScannerService } from '../../common/mcpResourceScannerService.js'; +import { UriIdentityService } from '../../../uriIdentity/common/uriIdentityService.js'; class TestMcpManagementService extends AbstractCommonMcpManagementService { @@ -42,6 +50,44 @@ class TestMcpManagementService extends AbstractCommonMcpManagementService { } } +class TestMcpResourceManagementService extends AbstractMcpResourceManagementService { + constructor(mcpResource: URI, fileService: FileService, uriIdentityService: UriIdentityService, mcpResourceScannerService: McpResourceScannerService) { + super( + mcpResource, + ConfigurationTarget.USER, + {} as IMcpGalleryService, + fileService, + uriIdentityService, + new NullLogService(), + mcpResourceScannerService, + ); + } + + public reload(): Promise { + return this.updateLocal(); + } + + override canInstall(_server: IGalleryMcpServer | IInstallableMcpServer): true | IMarkdownString { + throw new Error('Not supported'); + } + + protected override getLocalServerInfo(_name: string, _mcpServerConfig: IMcpServerConfiguration) { + return Promise.resolve(undefined); + } + + protected override installFromUri(_uri: URI): Promise { + throw new Error('Not supported'); + } + + override installFromGallery(_server: IGalleryMcpServer, _options?: InstallOptions): Promise { + throw new Error('Not supported'); + } + + override updateMetadata(_local: ILocalMcpServer, _server: IGalleryMcpServer): Promise { + throw new Error('Not supported'); + } +} + suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { let service: TestMcpManagementService; @@ -1073,3 +1119,74 @@ suite('McpManagementService - getMcpServerConfigurationFromManifest', () => { }); }); }); + +suite('McpResourceManagementService', () => { + const mcpResource = URI.from({ scheme: Schemas.inMemory, path: '/mcp.json' }); + let disposables: DisposableStore; + let fileService: FileService; + let service: TestMcpResourceManagementService; + + setup(async () => { + disposables = new DisposableStore(); + fileService = disposables.add(new FileService(new NullLogService())); + disposables.add(fileService.registerProvider(Schemas.inMemory, disposables.add(new InMemoryFileSystemProvider()))); + const uriIdentityService = disposables.add(new UriIdentityService(fileService)); + const scannerService = disposables.add(new McpResourceScannerService(fileService, uriIdentityService)); + service = disposables.add(new TestMcpResourceManagementService(mcpResource, fileService, uriIdentityService, scannerService)); + + await fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify({ + sandbox: { + network: { allowedDomains: ['example.com'] } + }, + servers: { + test: { + type: 'stdio', + command: 'node', + sandboxEnabled: true + } + } + }, null, '\t'))); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('fires update when root sandbox changes', async () => { + const initial = await service.getInstalled(); + assert.strictEqual(initial.length, 1); + assert.deepStrictEqual(initial[0].rootSandbox, { + network: { allowedDomains: ['example.com'] } + }); + + let updateCount = 0; + const updatePromise = new Promise(resolve => disposables.add(service.onDidUpdateMcpServers(e => { + assert.strictEqual(e.length, 1); + updateCount++; + resolve(); + }))); + + const updatedSandbox: IMcpSandboxConfiguration = { + network: { allowedDomains: ['changed.example.com'] } + }; + + await fileService.writeFile(mcpResource, VSBuffer.fromString(JSON.stringify({ + sandbox: updatedSandbox, + servers: { + test: { + type: 'stdio', + command: 'node', + sandboxEnabled: true + } + } + }, null, '\t'))); + await service.reload(); + await updatePromise; + const updated = await service.getInstalled(); + + assert.strictEqual(updateCount, 1); + assert.deepStrictEqual(updated[0].rootSandbox, updatedSandbox); + }); +}); diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 01831b024b921..aa48f0b90f236 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -308,6 +308,8 @@ .quick-input-list .quick-input-list-entry .quick-input-list-separator { margin-right: 4px; + font-size: var(--vscode-bodyFontSize-xSmall); + color: var(--vscode-descriptionForeground); /* separate from keybindings or actions */ } diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index dc7c596c346ca..44e53e09a6449 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -122,6 +122,7 @@ export const enum TerminalSettingId { FontLigaturesFallbackLigatures = 'terminal.integrated.fontLigatures.fallbackLigatures', EnableKittyKeyboardProtocol = 'terminal.integrated.enableKittyKeyboardProtocol', EnableWin32InputMode = 'terminal.integrated.enableWin32InputMode', + ExperimentalAiProfileGrouping = 'terminal.integrated.experimental.aiProfileGrouping', AllowInUntrustedWorkspace = 'terminal.integrated.allowInUntrustedWorkspace', // Developer/debug settings diff --git a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts index d644c5122b570..30d034ce089ed 100644 --- a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts +++ b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationOverviewView.ts @@ -24,11 +24,12 @@ import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyn import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.js'; import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; import { AICustomizationManagementEditor } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js'; -import { agentIcon, instructionsIcon, mcpServerIcon, promptIcon, skillIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; +import { agentIcon, instructionsIcon, mcpServerIcon, pluginIcon, promptIcon, skillIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { IEditorService, MODAL_GROUP } from '../../../../workbench/services/editor/common/editorService.js'; import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; +import { IAgentPluginService } from '../../../../workbench/contrib/chat/common/plugins/agentPluginService.js'; const $ = DOM.$; @@ -69,6 +70,7 @@ export class AICustomizationOverviewView extends ViewPane { @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, @IMcpService private readonly mcpService: IMcpService, + @IAgentPluginService private readonly agentPluginService: IAgentPluginService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -79,6 +81,7 @@ export class AICustomizationOverviewView extends ViewPane { { id: AICustomizationManagementSection.Instructions, label: localize('instructions', "Instructions"), icon: instructionsIcon, count: 0 }, { id: AICustomizationManagementSection.Prompts, label: localize('prompts', "Prompts"), icon: promptIcon, count: 0 }, { id: AICustomizationManagementSection.McpServers, label: localize('mcpServers', "MCP Servers"), icon: mcpServerIcon, count: 0 }, + { id: AICustomizationManagementSection.Plugins, label: localize('plugins', "Plugins"), icon: pluginIcon, count: 0 }, ); // Listen to changes @@ -186,6 +189,16 @@ export class AICustomizationOverviewView extends ViewPane { })); } + // Update plugin count reactively + const pluginSection = this.sections.find(s => s.id === AICustomizationManagementSection.Plugins); + if (pluginSection) { + this._register(autorun(reader => { + const plugins = this.agentPluginService.allPlugins.read(reader); + pluginSection.count = plugins.length; + this.updateCountElements(); + })); + } + this.updateCountElements(); } diff --git a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts index 47292441bdb7f..c055e550706af 100644 --- a/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts +++ b/src/vs/sessions/contrib/chat/browser/aiCustomizationWorkspaceService.ts @@ -69,7 +69,7 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization joinPath(userHome, '.agents'), ]; this._cliUserFilter = { - sources: [PromptsStorage.local, PromptsStorage.user], + sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.plugin], includedUserFileRoots: this._cliUserRoots, }; @@ -113,14 +113,15 @@ export class SessionsAICustomizationWorkspaceService implements IAICustomization AICustomizationManagementSection.Prompts, AICustomizationManagementSection.Hooks, AICustomizationManagementSection.McpServers, + AICustomizationManagementSection.Plugins, ]; private static readonly _hooksFilter: IStorageSourceFilter = { - sources: [PromptsStorage.local], + sources: [PromptsStorage.local, PromptsStorage.plugin], }; private static readonly _allUserRootsFilter: IStorageSourceFilter = { - sources: [PromptsStorage.local, PromptsStorage.user], + sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.plugin], }; getStorageSourceFilter(type: PromptsType): IStorageSourceFilter { diff --git a/src/vs/sessions/contrib/chat/browser/folderPicker.ts b/src/vs/sessions/contrib/chat/browser/folderPicker.ts index dbc253e1cbfa2..29411f26b7339 100644 --- a/src/vs/sessions/contrib/chat/browser/folderPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/folderPicker.ts @@ -17,7 +17,6 @@ import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs. import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { INewSession } from './newSession.js'; const STORAGE_KEY_LAST_FOLDER = 'agentSessions.lastPickedFolder'; const STORAGE_KEY_RECENT_FOLDERS = 'agentSessions.recentlyPickedFolders'; @@ -43,7 +42,6 @@ export class FolderPicker extends Disposable { private _selectedFolderUri: URI | undefined; private _recentlyPickedFolders: URI[] = []; - private _newSession: INewSession | undefined; private _triggerElement: HTMLElement | undefined; private readonly _renderDisposables = this._register(new DisposableStore()); @@ -52,14 +50,6 @@ export class FolderPicker extends Disposable { return this._selectedFolderUri; } - /** - * Sets the pending session that this picker writes to. - * When the user selects a folder, it calls `setRepoUri` on the session. - */ - setNewSession(session: INewSession | undefined): void { - this._newSession = session; - } - constructor( @IActionWidgetService private readonly actionWidgetService: IActionWidgetService, @IStorageService private readonly storageService: IStorageService, @@ -162,7 +152,7 @@ export class FolderPicker extends Disposable { } /** - * Programmatically set the selected folder. + * Programmatically set the selected folder (e.g. restoring draft state). */ setSelectedFolder(folderUri: URI): void { this._selectFolder(folderUri); @@ -181,7 +171,6 @@ export class FolderPicker extends Disposable { this._addToRecentlyPickedFolders(folderUri); this.storageService.store(STORAGE_KEY_LAST_FOLDER, folderUri.toString(), StorageScope.PROFILE, StorageTarget.MACHINE); this._updateTriggerLabel(this._triggerElement); - this._newSession?.setRepoUri(folderUri); this._onDidSelectFolder.fire(folderUri); } @@ -271,6 +260,20 @@ export class FolderPicker extends Disposable { return items; } + /** + * Removes a folder from the recently picked list and storage. + */ + removeFromRecents(folderUri: URI): void { + this._recentlyPickedFolders = this._recentlyPickedFolders.filter(f => !isEqual(f, folderUri)); + this.storageService.store(STORAGE_KEY_RECENT_FOLDERS, JSON.stringify(this._recentlyPickedFolders.map(f => f.toString())), StorageScope.PROFILE, StorageTarget.MACHINE); + // If this was the last picked folder, clear it + if (this._selectedFolderUri && isEqual(this._selectedFolderUri, folderUri)) { + this._selectedFolderUri = undefined; + this.storageService.remove(STORAGE_KEY_LAST_FOLDER, StorageScope.PROFILE); + this._updateTriggerLabel(this._triggerElement); + } + } + private _removeFolder(folderUri: URI): void { this._recentlyPickedFolders = this._recentlyPickedFolders.filter(f => !isEqual(f, folderUri)); this.storageService.store(STORAGE_KEY_RECENT_FOLDERS, JSON.stringify(this._recentlyPickedFolders.map(f => f.toString())), StorageScope.PROFILE, StorageTarget.MACHINE); diff --git a/src/vs/sessions/contrib/chat/browser/media/chatWidget.css b/src/vs/sessions/contrib/chat/browser/media/chatWidget.css index 247b4cdae062a..ef3a5d80a7913 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatWidget.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatWidget.css @@ -115,6 +115,11 @@ color: var(--vscode-icon-foreground); background: transparent !important; border: none !important; + cursor: pointer; +} + +.sessions-chat-send-button .monaco-button.disabled { + cursor: default; } .sessions-chat-send-button .monaco-button:not(.disabled):hover { diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 8f78d2590daef..47c38236ad38a 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -49,6 +49,7 @@ import { EnhancedModelPickerActionItem } from '../../../../workbench/contrib/cha import { IChatInputPickerOptions } from '../../../../workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.js'; import { IViewDescriptorService } from '../../../../workbench/common/views.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js'; import { ContextMenuController } from '../../../../editor/contrib/contextmenu/browser/contextmenu.js'; import { getSimpleEditorOptions } from '../../../../workbench/contrib/codeEditor/browser/simpleEditorOptions.js'; @@ -184,10 +185,10 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { @IContextKeyService private readonly contextKeyService: IContextKeyService, @ILogService private readonly logService: ILogService, @IHoverService private readonly hoverService: IHoverService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService, @IGitService private readonly gitService: IGitService, @IStorageService private readonly storageService: IStorageService, + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, ) { super(); this._history = this._register(this.instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat)); @@ -223,7 +224,11 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { this._focusEditor(); })); - this._register(this._folderPicker.onDidSelectFolder(() => { + this._register(this._folderPicker.onDidSelectFolder(async (folderUri) => { + const trusted = await this._requestFolderTrust(folderUri); + if (trusted) { + this._newSession.value?.setRepoUri(folderUri); + } this._updateDraftState(); this._focusEditor(); })); @@ -329,7 +334,16 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { private async _createNewSession(): Promise { const target = this._targetPicker.selectedTarget; - const defaultRepoUri = this._folderPicker.selectedFolderUri ?? this.workspaceContextService.getWorkspace().folders[0]?.uri; + let defaultRepoUri = this._folderPicker.selectedFolderUri; + + // For local targets, request workspace trust before creating the session + if (target === AgentSessionProviders.Background && defaultRepoUri) { + const trusted = await this._requestFolderTrust(defaultRepoUri); + if (!trusted) { + defaultRepoUri = undefined; + } + } + const resource = getResourceForNewChatSession({ type: target, position: this._options.sessionPosition ?? ChatSessionPosition.Sidebar, @@ -350,12 +364,10 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { // Wire pickers to the new session and disconnect inactive ones const target = this._targetPicker.selectedTarget; if (target === AgentSessionProviders.Background) { - this._folderPicker.setNewSession(session); this._isolationModePicker.setNewSession(session); this._branchPicker.setNewSession(session); this._repoPicker.setNewSession(undefined); } else { - this._folderPicker.setNewSession(undefined); this._isolationModePicker.setNewSession(undefined); this._branchPicker.setNewSession(undefined); this._repoPicker.setNewSession(session); @@ -587,7 +599,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { const target = this._targetPicker.selectedTarget; if (target === AgentSessionProviders.Background) { - return this._folderPicker.selectedFolderUri ?? this.workspaceContextService.getWorkspace().folders[0]?.uri; + return this._folderPicker.selectedFolderUri; } // For cloud targets, use the repo picker's selection @@ -1033,6 +1045,23 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget { } } + private async _requestFolderTrust(folderUri: URI): Promise { + const trusted = await this.workspaceTrustRequestService.requestResourcesTrust({ + uri: folderUri, + message: localize('trustFolderMessage', "An agent session will be able to read files, run commands, and make changes in this folder."), + }); + if (!trusted) { + this._folderPicker.removeFromRecents(folderUri); + const previousFolderUri = this._newSession.value?.repoUri; + if (previousFolderUri) { + this._folderPicker.setSelectedFolder(previousFolderUri); + } else { + this._folderPicker.clearSelection(); + } + } + return !!trusted; + } + private _resolveDefaultTarget(options: INewChatWidgetOptions): AgentSessionProviders { const draft = this._getDraftState(); diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index 0effc9b662a5c..e94d1317988a8 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -19,7 +19,6 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'breadcrumbs.enabled': false, - 'diffEditor.renderSideBySide': false, 'diffEditor.hideUnchangedRegions.enabled': true, 'extensions.ignoreRecommendations': true, diff --git a/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts b/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts index 2aed42bfd4cf4..9aaaac7acb32e 100644 --- a/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts +++ b/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts @@ -73,6 +73,7 @@ class GitSyncContribution extends Disposable implements IWorkbenchContribution { const behind = head.behind ?? 0; const hasSyncChanges = ahead > 0 || behind > 0; contextKey.set(hasSyncChanges); + this._syncActionDisposable.clear(); this._syncActionDisposable.value = registerSyncAction(behind, ahead, isSyncing, (syncing) => { this._isSyncing.set(syncing, undefined); }); diff --git a/src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts b/src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts index c4e89d70e17a7..58c814528b3d5 100644 --- a/src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts @@ -22,6 +22,7 @@ import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.j import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { Menus } from '../../../browser/menus.js'; import { getCustomizationTotalCount } from './customizationCounts.js'; +import { IAgentPluginService } from '../../../../workbench/contrib/chat/common/plugins/agentPluginService.js'; const $ = DOM.$; @@ -42,6 +43,7 @@ export class AICustomizationShortcutsWidget extends Disposable { @IMcpService private readonly mcpService: IMcpService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService, + @IAgentPluginService private readonly agentPluginService: IAgentPluginService, ) { super(); @@ -93,7 +95,7 @@ export class AICustomizationShortcutsWidget extends Disposable { let updateCountRequestId = 0; const updateHeaderTotalCount = async () => { const requestId = ++updateCountRequestId; - const totalCount = await getCustomizationTotalCount(this.promptsService, this.mcpService, this.workspaceService, this.workspaceContextService); + const totalCount = await getCustomizationTotalCount(this.promptsService, this.mcpService, this.workspaceService, this.workspaceContextService, this.agentPluginService); if (requestId !== updateCountRequestId) { return; } diff --git a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts index 682c73edc6bc9..ad30f5c04ad78 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationCounts.ts @@ -13,6 +13,7 @@ import { IPromptsService, PromptsStorage } from '../../../../workbench/contrib/c import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; import { IAICustomizationWorkspaceService, applyStorageSourceFilter, IStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; import { parseHooksFromFile } from '../../../../workbench/contrib/chat/common/promptSyntax/hookCompatibility.js'; +import { IAgentPluginService } from '../../../../workbench/contrib/chat/common/plugins/agentPluginService.js'; import { parse as parseJSONC } from '../../../../base/common/jsonc.js'; export interface ISourceCounts { @@ -136,6 +137,7 @@ export async function getCustomizationTotalCount( mcpService: IMcpService, workspaceService: IAICustomizationWorkspaceService, workspaceContextService: IWorkspaceContextService, + agentPluginService?: IAgentPluginService, ): Promise { const types: PromptsType[] = [PromptsType.agent, PromptsType.skill, PromptsType.instructions, PromptsType.prompt, PromptsType.hook]; const results = await Promise.all(types.map(type => { @@ -143,5 +145,6 @@ export async function getCustomizationTotalCount( return getSourceCounts(promptsService, type, filter, workspaceContextService, workspaceService) .then(counts => getSourceCountsTotal(counts, filter)); })); - return results.reduce((sum, n) => sum + n, 0) + mcpService.servers.get().length; + const pluginCount = agentPluginService?.allPlugins.get().length ?? 0; + return results.reduce((sum, n) => sum + n, 0) + mcpService.servers.get().length + pluginCount; } diff --git a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts index ba97537a9b820..350a9fa19b850 100644 --- a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts +++ b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts @@ -20,7 +20,7 @@ import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyn import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js'; import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; import { Menus } from '../../../browser/menus.js'; -import { agentIcon, instructionsIcon, mcpServerIcon, promptIcon, skillIcon, hookIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; +import { agentIcon, instructionsIcon, mcpServerIcon, pluginIcon, promptIcon, skillIcon, hookIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; import { ActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction } from '../../../../base/common/actions.js'; import { $, append } from '../../../../base/browser/dom.js'; @@ -33,6 +33,7 @@ import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultS import { getSourceCounts, getSourceCountsTotal } from './customizationCounts.js'; import { IEditorService, MODAL_GROUP } from '../../../../workbench/services/editor/common/editorService.js'; import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { IAgentPluginService } from '../../../../workbench/contrib/chat/common/plugins/agentPluginService.js'; export interface ICustomizationItemConfig { readonly id: string; @@ -41,6 +42,7 @@ export interface ICustomizationItemConfig { readonly section: AICustomizationManagementSection; readonly promptType?: PromptsType; readonly isMcp?: boolean; + readonly isPlugins?: boolean; } export const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ @@ -86,6 +88,13 @@ export const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ section: AICustomizationManagementSection.McpServers, isMcp: true, }, + { + id: 'sessions.customization.plugins', + label: localize('plugins', "Plugins"), + icon: pluginIcon, + section: AICustomizationManagementSection.Plugins, + isPlugins: true, + }, ]; /** @@ -109,6 +118,7 @@ export class CustomizationLinkViewItem extends ActionViewItem { @ISessionsManagementService private readonly _activeSessionService: ISessionsManagementService, @IAICustomizationWorkspaceService private readonly _workspaceService: IAICustomizationWorkspaceService, @IFileService private readonly _fileService: IFileService, + @IAgentPluginService private readonly _agentPluginService: IAgentPluginService, ) { super(undefined, action, { ...options, icon: false, label: false }); this._viewItemDisposables = this._register(new DisposableStore()); @@ -152,6 +162,10 @@ export class CustomizationLinkViewItem extends ActionViewItem { this._mcpService.servers.read(reader); this._updateCounts(); })); + this._viewItemDisposables.add(autorun(reader => { + this._agentPluginService.allPlugins.read(reader); + this._updateCounts(); + })); this._viewItemDisposables.add(this._workspaceContextService.onDidChangeWorkspaceFolders(() => this._updateCounts())); this._viewItemDisposables.add(autorun(reader => { this._activeSessionService.activeSession.read(reader); @@ -183,6 +197,9 @@ export class CustomizationLinkViewItem extends ActionViewItem { } else if (this._config.isMcp) { const total = this._mcpService.servers.get().length; this._renderTotalCount(this._countContainer, total); + } else if (this._config.isPlugins) { + const total = this._agentPluginService.allPlugins.get().length; + this._renderTotalCount(this._countContainer, total); } } diff --git a/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts b/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts index 71d5b630d1149..6fc39f227257f 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts @@ -19,6 +19,7 @@ import { PromptsType } from '../../../../../workbench/contrib/chat/common/prompt import { ILanguageModelsService } from '../../../../../workbench/contrib/chat/common/languageModels.js'; import { IMcpServer, IMcpService } from '../../../../../workbench/contrib/mcp/common/mcpTypes.js'; import { IAICustomizationWorkspaceService, IStorageSourceFilter } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; +import { IAgentPluginService } from '../../../../../workbench/contrib/chat/common/plugins/agentPluginService.js'; import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; import { AICustomizationShortcutsWidget } from '../../browser/aiCustomizationShortcutsWidget.js'; import { CUSTOMIZATION_ITEMS, CustomizationLinkViewItem } from '../../browser/customizationsToolbar.contribution.js'; @@ -191,6 +192,10 @@ function renderWidget(ctx: ComponentFixtureContext, options?: { mcpServerCount?: reg.defineInstance(IMcpService, createMockMcpService(options?.mcpServerCount ?? 0)); reg.defineInstance(IAICustomizationWorkspaceService, createMockWorkspaceService()); reg.defineInstance(IWorkspaceContextService, createMockWorkspaceContextService()); + reg.defineInstance(IAgentPluginService, new class extends mock() { + override readonly plugins = observableValue('mockPlugins', []); + override readonly allPlugins = observableValue('mockAllPlugins', []); + }()); // Additional services needed by CustomizationLinkViewItem reg.defineInstance(ILanguageModelsService, new class extends mock() { override readonly onDidChangeLanguageModels = Event.None; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 61a6ce830c289..9c5adf7bc0ddb 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -3440,6 +3440,7 @@ export namespace ChatAgentRequest { editedFileEvents: request.editedFileEvents, modeInstructions: request.modeInstructions?.content, modeInstructions2: ChatRequestModeInstructions.to(request.modeInstructions), + permissionLevel: request.permissionLevel, subAgentInvocationId: request.subAgentInvocationId, subAgentName: request.subAgentName, parentRequestId: request.parentRequestId, @@ -4052,6 +4053,7 @@ export namespace McpServerDefinition { command: item.command, env: item.env, envFile: undefined, + sandbox: undefined } ); } diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index be80260090836..5af4f540bac41 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -9,7 +9,6 @@ import { IConfirmation, IConfirmationResult, IInputResult, ICheckbox, IInputElem import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import Severity from '../../../../base/common/severity.js'; -import { IButton } from '../../../../base/browser/ui/button/button.js'; import { Dialog, IDialogResult } from '../../../../base/browser/ui/dialog/dialog.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; @@ -106,14 +105,8 @@ export class BrowserDialogHandler extends AbstractDialogHandler { parent.appendChild(result.element); result.element.classList.add(...(markdownDetail.classes || [])); }); - customOptions.renderBody?.(parent, dialogDisposables); } : undefined; - const buttonOptions = customOptions?.buttonOptions?.map(opt => opt ? { - sublabel: opt.sublabel, - styleButton: opt.styleButton ? (button: IButton) => opt.styleButton!(button) : undefined - } : undefined) ?? customOptions?.buttonDetails?.map(detail => ({ sublabel: detail })); - const dialog = new Dialog( this.layoutService.activeContainer, message, @@ -125,7 +118,7 @@ export class BrowserDialogHandler extends AbstractDialogHandler { renderBody, icon: customOptions?.icon, disableCloseAction: customOptions?.disableCloseAction, - buttonOptions, + buttonOptions: customOptions?.buttonDetails?.map(detail => ({ sublabel: detail })), checkboxLabel: checkbox?.label, checkboxChecked: checkbox?.checked, inputs diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index ca2d962ea0dd6..c172d537754ca 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -34,7 +34,7 @@ import { IBoundarySashes } from '../../../../base/browser/ui/sash/sash.js'; import { IHostService } from '../../../services/host/browser/host.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { EditorPartMaximizedEditorGroupContext, EditorPartMultipleEditorGroupsContext } from '../../../common/contextkeys.js'; +import { EditorPartMaximizedEditorGroupContext, EditorPartMultipleEditorGroupsContext, EditorTabsVisibleContext } from '../../../common/contextkeys.js'; import { mainWindow } from '../../../../base/browser/window.js'; export interface IEditorPartUIState { @@ -1039,6 +1039,7 @@ export class EditorPart extends Part implements IEditorPart, protected handleContextKeys(): void { const multipleEditorGroupsContext = EditorPartMultipleEditorGroupsContext.bindTo(this.scopedContextKeyService); const maximizedEditorGroupContext = EditorPartMaximizedEditorGroupContext.bindTo(this.scopedContextKeyService); + const editorTabsVisibleContext = EditorTabsVisibleContext.bindTo(this.scopedContextKeyService); const updateContextKeys = () => { const groupCount = this.count; @@ -1055,11 +1056,17 @@ export class EditorPart extends Part implements IEditorPart, } }; + const updateEditorTabsVisibleContext = () => { + editorTabsVisibleContext.set(this.partOptions.showTabs === 'multiple'); + }; + updateContextKeys(); + updateEditorTabsVisibleContext(); this._register(this.onDidAddGroup(() => updateContextKeys())); this._register(this.onDidRemoveGroup(() => updateContextKeys())); this._register(this.onDidChangeGroupMaximized(() => updateContextKeys())); + this._register(this.onDidChangeEditorPartOptions(() => updateEditorTabsVisibleContext())); } private setupDragAndDropSupport(parent: HTMLElement, container: HTMLElement): void { diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 143a09c1bf17c..7c7df50fd3846 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -549,6 +549,11 @@ export const PANEL_STICKY_SCROLL_BORDER = registerColor('panelStickyScroll.borde export const PANEL_STICKY_SCROLL_SHADOW = registerColor('panelStickyScroll.shadow', scrollbarShadow, localize('panelStickyScrollShadow', "Shadow color of sticky scroll in the panel.")); +// < --- Browser --- > + +export const BROWSER_BORDER = registerColor('browser.border', TAB_BORDER, localize('browserBorder', "Border color for integrated browser pages.")); + + // < --- Profiles --- > export const PROFILE_BADGE_BACKGROUND = registerColor('profileBadge.background', { diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index 1a551c6c31c57..a9983e9e46800 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -336,9 +336,13 @@ export class BrowserEditor extends EditorPane { this._browserContainer.tabIndex = 0; // make focusable this._browserContainerWrapper.appendChild(this._browserContainer); + // Create additional wrapper around placeholder contents for applying border radius clipping. + const placeholderContents = $('.browser-placeholder-contents'); + this._browserContainer.appendChild(placeholderContents); + // Create placeholder screenshot (background placeholder when WebContentsView is hidden) this._placeholderScreenshot = $('.browser-placeholder-screenshot'); - this._browserContainer.appendChild(this._placeholderScreenshot); + placeholderContents.appendChild(this._placeholderScreenshot); // Create overlay pause container (hidden by default via CSS) this._overlayPauseContainer = $('.browser-overlay-paused'); @@ -348,16 +352,16 @@ export class BrowserEditor extends EditorPane { overlayPauseMessage.appendChild(this._overlayPauseHeading); overlayPauseMessage.appendChild(this._overlayPauseDetail); this._overlayPauseContainer.appendChild(overlayPauseMessage); - this._browserContainer.appendChild(this._overlayPauseContainer); + placeholderContents.appendChild(this._overlayPauseContainer); // Create error container (hidden by default) this._errorContainer = $('.browser-error-container'); this._errorContainer.style.display = 'none'; - this._browserContainer.appendChild(this._errorContainer); + placeholderContents.appendChild(this._errorContainer); // Create welcome container (shown when no URL is loaded) this._welcomeContainer = this.createWelcomeContainer(); - this._browserContainer.appendChild(this._welcomeContainer); + placeholderContents.appendChild(this._welcomeContainer); this._register(addDisposableListener(this._browserContainer, EventType.FOCUS, (event) => { // When the browser container gets focus, make sure the browser view also gets focused. @@ -399,7 +403,7 @@ export class BrowserEditor extends EditorPane { this._storageScopeContext.set(this._model.storageScope); this._devToolsOpenContext.set(this._model.isDevToolsOpen); - this._updateSharingState(); + this._updateSharingState(true); // Update find widget with new model this._findWidget.rawValue?.setModel(this._model); @@ -411,10 +415,10 @@ export class BrowserEditor extends EditorPane { // Listen for sharing state changes on the model this._inputDisposables.add(this._model.onDidChangeSharedWithAgent(() => { - this._updateSharingState(); + this._updateSharingState(false); })); this._inputDisposables.add(watchForAgentSharingContextChanges(this.contextKeyService)(() => { - this._updateSharingState(); + this._updateSharingState(false); })); // Initialize UI state and context keys from model @@ -661,11 +665,12 @@ export class BrowserEditor extends EditorPane { return this._model?.url; } - private _updateSharingState(): void { + private _updateSharingState(isInitialState: boolean): void { const sharingEnabled = this.contextKeyService.contextMatchesRules(canShareBrowserWithAgentContext); const isShared = sharingEnabled && !!this._model && this._model.sharedWithAgent; - this._browserContainerWrapper.classList.toggle('shared', isShared); + this._browserContainer.classList.toggle('animate', !isInitialState); + this._browserContainer.classList.toggle('shared', isShared); this._navigationBar.setShared(isShared); } @@ -1182,13 +1187,16 @@ export class BrowserEditor extends EditorPane { this.checkOverlays(); const containerRect = this._browserContainer.getBoundingClientRect(); + const cornerRadius = this.window.getComputedStyle(this._browserContainer).borderTopLeftRadius ?? '0'; + void this._model.layout({ windowId: this.group.windowId, x: containerRect.left, y: containerRect.top, width: containerRect.width, height: containerRect.height, - zoomFactor: getZoomFactor(this.window) + zoomFactor: getZoomFactor(this.window), + cornerRadius: parseFloat(cornerRadius) }); } } diff --git a/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css b/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css index 8ad449ee69d56..d80361715ce05 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css +++ b/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css @@ -3,6 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +@property --animation-angle { + syntax: ''; + inherits: false; + initial-value: 135deg; +} + +@property --animation-opacity { + syntax: ''; + inherits: false; + initial-value: 0%; +} + +@keyframes browser-shared-border-spin { + from { + --animation-angle: 135deg; + } + to { + --animation-angle: 495deg; + } +} + .browser-root { display: flex; flex-direction: column; @@ -13,7 +34,6 @@ display: flex; align-items: center; padding: 6px 8px; - border-bottom: 1px solid var(--vscode-widget-border); background-color: var(--vscode-editor-background); flex-shrink: 0; gap: 8px; @@ -27,10 +47,13 @@ .actions-container { gap: 4px; - margin-right: 4px; } } + .browser-actions-toolbar { + margin-right: 4px; + } + .browser-url-container { flex: 1; display: flex; @@ -68,6 +91,12 @@ color: var(--vscode-descriptionForeground); white-space: nowrap; gap: 4px; + outline: none !important; + + &:focus-visible { + outline: 1px solid var(--vscode-focusBorder) !important; + outline-offset: 0px !important; + } .codicon { margin: 0; @@ -101,37 +130,7 @@ flex: 1; min-height: 0; position: relative; - z-index: 0; /* Important: creates a new stacking context for the gradient border trick */ - - &.shared { - &::before { - content: ''; - position: absolute; - top: -2px; - left: 0; - right: 0; - bottom: 0; - z-index: -2; - background: linear-gradient(135deg in lab, - color-mix(in srgb, #51a2ff 100%, transparent), - color-mix(in srgb, #4af0c0 100%, transparent), - color-mix(in srgb, #b44aff 100%, transparent) - ) !important; - pointer-events: none; - } - - &::after { - content: ''; - position: absolute; - top: 1px; - left: 3px; - right: 3px; - bottom: 3px; - z-index: -1; - background-color: var(--vscode-editor-background); - pointer-events: none; - } - } + margin-top: 1px; } .browser-container { @@ -143,12 +142,78 @@ * which would cause visible shifts when swapping between the live * view and the placeholder screenshot. */ - width: round(down, 100% - 4px, calc(1px / var(--zoom-factor, 1))); - height: round(down, 100% - 2px, calc(1px / var(--zoom-factor, 1))); + width: round(down, 100% - 8px, calc(1px / var(--zoom-factor, 1))); + height: round(down, 100% - 4px, calc(1px / var(--zoom-factor, 1))); margin: 0 auto; overflow: visible; + border-radius: var(--vscode-cornerRadius-medium); position: relative; outline: none !important; + z-index: 0; /* Important: creates a new stacking context for the gradient border trick */ + + &::before { + content: ''; + position: absolute; + --animation-angle: 135deg; + --animation-opacity: 0%; + top: -1px; + left: -1px; + right: -1px; + bottom: -1px; + z-index: -2; + border-radius: var(--vscode-cornerRadius-medium); + background: conic-gradient(from var(--animation-angle), + color-mix(in srgb, #b44aff var(--animation-opacity), var(--vscode-browser-border, transparent)), + color-mix(in srgb, #4af0c0 var(--animation-opacity), var(--vscode-browser-border, transparent)), + color-mix(in srgb, #51a2ff var(--animation-opacity), var(--vscode-browser-border, transparent)), + color-mix(in srgb, #4af0c0 var(--animation-opacity), var(--vscode-browser-border, transparent)), + color-mix(in srgb, #b44aff var(--animation-opacity), var(--vscode-browser-border, transparent)) + ); + pointer-events: none; + } + + &.shared::before { + top: -3px; + left: -3px; + right: -3px; + bottom: -3px; + --animation-angle: 495deg; + --animation-opacity: 100%; + } + + &.animate::before { + transition: top 350ms cubic-bezier(0.2, 0, 0, 1), + left 350ms cubic-bezier(0.2, 0, 0, 1), + right 350ms cubic-bezier(0.2, 0, 0, 1), + bottom 350ms cubic-bezier(0.2, 0, 0, 1), + --animation-opacity 350ms cubic-bezier(0.2, 0, 0, 1); + } + + &.shared.animate::before { + animation: browser-shared-border-spin 1500ms cubic-bezier(0, 0.2, 0, 1) 1 forwards, + browser-shared-border-spin 45s linear 1500ms infinite; + } + + &::after { + content: ''; + position: absolute; + top: 1px; + left: 1px; + right: 1px; + bottom: 1px; + z-index: -1; + border-radius: var(--vscode-cornerRadius-medium); + background-color: var(--vscode-editor-background); + pointer-events: none; + } + } + + .browser-placeholder-contents { + width: 100%; + height: 100%; + position: relative; + overflow: hidden; + border-radius: var(--vscode-cornerRadius-medium); } .browser-placeholder-screenshot { @@ -282,8 +347,9 @@ border-radius: 0; border: none; top: 0 !important; - padding: 6px 8px 6px 12px; + padding: 2px 8px 6px 12px; transition: none; + background: none !important; &:not(.visible) { display: none; @@ -291,11 +357,13 @@ .monaco-sash { width: 2px !important; - border-radius: 0; + height: calc(100% - 4px); + border-radius: var(--vscode-cornerRadius-circle); &::before { width: var(--vscode-sash-hover-size); left: calc(50% - (var(--vscode-sash-hover-size) / 2)); + border-radius: var(--vscode-cornerRadius-circle); } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts index 23b4712e2a50f..42d67d3b3a579 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts @@ -63,12 +63,6 @@ export function registerChatCopyActions() { when: ChatContextKeys.responseIsFiltered.negate(), group: 'copy', }, - { - id: MenuId.ChatMessageTitle, - group: 'navigation', - order: 5, - when: ChatContextKeys.responseIsFiltered.negate(), - }, { id: MenuId.ChatMessageFooter, group: 'navigation', diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 4d7b9f7fa8f18..b1d898bbcb174 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -22,11 +22,12 @@ import { KeybindingWeight } from '../../../../../platform/keybinding/common/keyb import { ILogService } from '../../../../../platform/log/common/log.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; +import { IsSessionsWindowContext } from '../../../../common/contextkeys.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { getModeNameForTelemetry, IChatMode, IChatModeService } from '../../common/chatModes.js'; import { chatVariableLeader } from '../../common/requestParser/chatParserTypes.js'; import { ChatStopCancellationNoopClassification, ChatStopCancellationNoopEvent, ChatStopCancellationNoopEventName, IChatService } from '../../common/chatService/chatService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind, } from '../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { ILanguageModelToolsService } from '../../common/tools/languageModelToolsService.js'; import { isInClaudeAgentsFolder } from '../../common/promptSyntax/config/promptFileLocations.js'; @@ -185,8 +186,19 @@ const requestInProgressOrPendingToolCall = ContextKeyExpr.or( ChatContextKeys.Editing.hasToolConfirmation, ChatContextKeys.Editing.hasQuestionCarousel, ); +const requestInProgressWithoutInput = ContextKeyExpr.and( + ChatContextKeys.requestInProgress, + ChatContextKeys.inputHasText.negate(), +); +const pendingToolCall = ContextKeyExpr.or( + ChatContextKeys.Editing.hasToolConfirmation, + ContextKeyExpr.and(ChatContextKeys.Editing.hasQuestionCarousel, ChatContextKeys.inputHasText.negate()), +); +const noQuestionCarouselOrHasInput = ContextKeyExpr.or( + ChatContextKeys.Editing.hasQuestionCarousel.negate(), + ChatContextKeys.inputHasText, +); const whenNotInProgress = ChatContextKeys.requestInProgress.negate(); -const whenNoRequestOrPendingToolCall = requestInProgressOrPendingToolCall!.negate(); export class ChatSubmitAction extends SubmitAction { static readonly ID = 'workbench.action.chat.submit'; @@ -195,7 +207,7 @@ export class ChatSubmitAction extends SubmitAction { const menuCondition = ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask); const precondition = ContextKeyExpr.and( ChatContextKeys.inputHasText, - whenNoRequestOrPendingToolCall, + whenNotInProgress, ChatContextKeys.chatSessionOptionsValid, ); @@ -224,9 +236,10 @@ export class ChatSubmitAction extends SubmitAction { id: MenuId.ChatExecute, order: 4, when: ContextKeyExpr.and( - whenNoRequestOrPendingToolCall, + whenNotInProgress, menuCondition, ChatContextKeys.withinEditSessionDiff.negate(), + noQuestionCarouselOrHasInput, ), group: 'navigation', alt: { @@ -240,7 +253,7 @@ export class ChatSubmitAction extends SubmitAction { order: 4, when: ContextKeyExpr.and( ContextKeyExpr.or(ctxHasEditorModification.negate(), ChatContextKeys.inputHasText), - whenNoRequestOrPendingToolCall, + whenNotInProgress, ChatContextKeys.requestInProgress.negate(), menuCondition ), @@ -440,6 +453,44 @@ export class OpenModelPickerAction extends Action2 { } } } + +export class OpenPermissionPickerAction extends Action2 { + static readonly ID = 'workbench.action.chat.openPermissionPicker'; + + constructor() { + super({ + id: OpenPermissionPickerAction.ID, + title: localize2('interactive.openPermissionPicker.label', "Open Permission Picker"), + tooltip: localize('setPermissionLevel', "Set Permissions"), + category: CHAT_CATEGORY, + f1: false, + precondition: ChatContextKeys.enabled, + menu: { + id: MenuId.ChatInputSecondary, + order: 10, + group: 'navigation', + when: + ContextKeyExpr.and( + ChatContextKeys.enabled, + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), + ChatContextKeys.chatModeKind.notEqualsTo(ChatModeKind.Ask), + ChatContextKeys.inQuickChat.negate(), + ChatContextKeys.lockedToCodingAgent.negate(), + IsSessionsWindowContext.negate(), + ) + } + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const widgetService = accessor.get(IChatWidgetService); + const widget = widgetService.lastFocusedWidget; + if (widget) { + widget.input.openPermissionPicker(); + } + } +} + export class OpenModePickerAction extends Action2 { static readonly ID = 'workbench.action.chat.openModePicker'; @@ -508,6 +559,18 @@ export class OpenSessionTargetPickerAction extends Action2 { ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), ChatContextKeys.inQuickChat.negate(), + ChatContextKeys.chatSessionIsEmpty, + IsSessionsWindowContext), + group: 'navigation', + }, + { + id: MenuId.ChatInputSecondary, + order: 0, + when: ContextKeyExpr.and( + ChatContextKeys.enabled, + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), + ChatContextKeys.inQuickChat.negate(), + IsSessionsWindowContext.negate(), ChatContextKeys.chatSessionIsEmpty), group: 'navigation', }, @@ -543,7 +606,19 @@ export class OpenDelegationPickerAction extends Action2 { ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), ChatContextKeys.inQuickChat.negate(), - ChatContextKeys.chatSessionIsEmpty.negate()), + ChatContextKeys.chatSessionIsEmpty.negate(), + IsSessionsWindowContext), + group: 'navigation', + }, + { + id: MenuId.ChatInputSecondary, + order: 0.5, + when: ContextKeyExpr.and( + ChatContextKeys.enabled, + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), + ChatContextKeys.inQuickChat.negate(), + ChatContextKeys.chatSessionIsEmpty.negate(), + IsSessionsWindowContext.negate()), group: 'navigation', }, ] @@ -576,7 +651,18 @@ export class OpenWorkspacePickerAction extends Action2 { order: 0.6, when: ContextKeyExpr.and( ChatContextKeys.inAgentSessionsWelcome, - ChatContextKeys.chatSessionType.isEqualTo('local') + ChatContextKeys.chatSessionType.isEqualTo('local'), + IsSessionsWindowContext + ), + group: 'navigation', + }, + { + id: MenuId.ChatInputSecondary, + order: 0.6, + when: ContextKeyExpr.and( + ChatContextKeys.inAgentSessionsWelcome, + ChatContextKeys.chatSessionType.isEqualTo('local'), + IsSessionsWindowContext.negate() ), group: 'navigation', }, @@ -594,7 +680,7 @@ export class ChatSessionPrimaryPickerAction extends Action2 { constructor() { super({ id: ChatSessionPrimaryPickerAction.ID, - title: localize2('interactive.openChatSessionPrimaryPicker.label', "Open Model Picker"), + title: localize2('interactive.openChatSessionPrimaryPicker.label', "Open Primary Session Picker"), category: CHAT_CATEGORY, f1: false, precondition: ChatContextKeys.enabled, @@ -657,7 +743,7 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { constructor() { const notInProgressOrEditing = ContextKeyExpr.and( - ContextKeyExpr.or(whenNoRequestOrPendingToolCall, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.Sent)), + ContextKeyExpr.or(whenNotInProgress, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.Sent)), ChatContextKeys.editingRequestType.notEqualsTo(ChatContextKeys.EditingRequestType.QueueOrSteer) ); @@ -681,7 +767,8 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { order: 4, when: ContextKeyExpr.and( notInProgressOrEditing, - menuCondition), + menuCondition, + noQuestionCarouselOrHasInput), group: 'navigation', alt: { id: 'workbench.action.chat.sendToNewChat', @@ -839,8 +926,7 @@ export class CancelAction extends Action2 { menu: [{ id: MenuId.ChatExecute, when: ContextKeyExpr.and( - requestInProgressOrPendingToolCall, - ChatContextKeys.inputHasText.negate(), + ContextKeyExpr.or(requestInProgressWithoutInput, pendingToolCall), ChatContextKeys.remoteJobCreating.negate(), ChatContextKeys.currentlyEditing.negate(), ), @@ -954,6 +1040,7 @@ export function registerChatExecuteActions() { registerAction2(ToggleChatModeAction); registerAction2(SwitchToNextModelAction); registerAction2(OpenModelPickerAction); + registerAction2(OpenPermissionPickerAction); registerAction2(OpenModePickerAction); registerAction2(OpenSessionTargetPickerAction); registerAction2(OpenDelegationPickerAction); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts index 701177fb30d4e..6606748d653f4 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts @@ -18,12 +18,7 @@ import { IChatWidgetService } from '../chat.js'; import { CHAT_CATEGORY } from './chatActions.js'; const queuingActionsPresent = ContextKeyExpr.and( - ContextKeyExpr.or( - ChatContextKeys.requestInProgress, - ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.QueueOrSteer), - ChatContextKeys.Editing.hasQuestionCarousel, - ChatContextKeys.Editing.hasToolConfirmation, - ), + ContextKeyExpr.or(ChatContextKeys.requestInProgress, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.QueueOrSteer)), ChatContextKeys.editingRequestType.notEqualsTo(ChatContextKeys.EditingRequestType.Sent), ); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 474df4e970ba4..e890fb82a04c6 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -43,7 +43,7 @@ import { IPathService } from '../../../../services/path/common/pathService.js'; import { generateCustomizationDebugReport } from './aiCustomizationDebugPanel.js'; import { parseHooksFromFile } from '../../common/promptSyntax/hookCompatibility.js'; import { formatHookCommandLabel } from '../../common/promptSyntax/hookSchema.js'; -import { HOOK_METADATA } from '../../common/promptSyntax/hookTypes.js'; +import { HookType, HOOK_METADATA } from '../../common/promptSyntax/hookTypes.js'; import { parse as parseJSONC } from '../../../../../base/common/json.js'; import { Schemas } from '../../../../../base/common/network.js'; import { OS } from '../../../../../base/common/platform.js'; @@ -65,6 +65,8 @@ export interface IAICustomizationListItem { readonly description?: string; readonly storage: PromptsStorage; readonly promptType: PromptsType; + /** When set, overrides `storage` for display grouping purposes. */ + readonly groupKey?: string; nameMatches?: IMatch[]; descriptionMatches?: IMatch[]; } @@ -75,7 +77,7 @@ export interface IAICustomizationListItem { interface IGroupHeaderEntry { readonly type: 'group-header'; readonly id: string; - readonly storage: PromptsStorage; + readonly groupKey: string; readonly label: string; readonly icon: ThemeIcon; readonly count: number; @@ -311,7 +313,7 @@ export class AICustomizationListWidget extends Disposable { private allItems: IAICustomizationListItem[] = []; private displayEntries: IListEntry[] = []; private searchQuery: string = ''; - private readonly collapsedGroups = new Set(); + private readonly collapsedGroups = new Set(); private readonly dropdownActionDisposables = this._register(new DisposableStore()); private readonly delayedFilter = new Delayer(200); @@ -723,7 +725,8 @@ export class AICustomizationListWidget extends Disposable { * Loads items for the current section. */ private async loadItems(): Promise { - const promptType = sectionToPromptType(this.currentSection); + const section = this.currentSection; + const promptType = sectionToPromptType(section); const items: IAICustomizationListItem[] = []; @@ -827,6 +830,37 @@ export class AICustomizationListWidget extends Disposable { }); } } + + // Also include hooks defined in agent frontmatter (not in sessions window) + // TODO: add this back when Copilot CLI supports this + const agents = !this.workspaceService.isSessionsWindow ? await this.promptsService.getCustomAgents(CancellationToken.None) : []; + for (const agent of agents) { + if (!agent.hooks) { + continue; + } + for (const hookType of Object.values(HookType)) { + const hookCommands = agent.hooks[hookType]; + if (!hookCommands || hookCommands.length === 0) { + continue; + } + const hookMeta = HOOK_METADATA[hookType]; + for (let i = 0; i < hookCommands.length; i++) { + const hook = hookCommands[i]; + const cmdLabel = formatHookCommandLabel(hook, OS); + const truncatedCmd = cmdLabel.length > 60 ? cmdLabel.substring(0, 57) + '...' : cmdLabel; + items.push({ + id: `${agent.uri.toString()}#hook:${hookType}[${i}]`, + uri: agent.uri, + name: hookMeta?.label ?? hookType, + filename: basename(agent.uri), + description: `${agent.name}: ${truncatedCmd || localize('hookUnset', "(unset)")}`, + storage: agent.source.storage, + groupKey: 'agents', + promptType, + }); + } + } + } } else { // For instructions, fetch prompt files and group by storage const promptFiles = await this.promptsService.listPromptFiles(promptType, CancellationToken.None); @@ -886,6 +920,10 @@ export class AICustomizationListWidget extends Disposable { // Sort items by name items.sort((a, b) => a.name.localeCompare(b.name)); + if (this.currentSection !== section) { + return; // section changed while loading + } + this.allItems = items; this.filterItems(); this._onDidChangeItemCount.fire(items.length); @@ -940,15 +978,17 @@ export class AICustomizationListWidget extends Disposable { // Group items by storage const promptType = sectionToPromptType(this.currentSection); const visibleSources = new Set(this.workspaceService.getStorageSourceFilter(promptType).sources); - const groups: { storage: PromptsStorage; label: string; icon: ThemeIcon; description: string; items: IAICustomizationListItem[] }[] = [ - { storage: PromptsStorage.local, label: localize('workspaceGroup', "Workspace"), icon: workspaceIcon, description: localize('workspaceGroupDescription', "Customizations stored as files in your project folder and shared with your team via version control."), items: [] }, - { storage: PromptsStorage.user, label: localize('userGroup', "User"), icon: userIcon, description: localize('userGroupDescription', "Customizations stored locally on your machine in a central location. Private to you and available across all projects."), items: [] }, - { storage: PromptsStorage.extension, label: localize('extensionGroup', "Extensions"), icon: extensionIcon, description: localize('extensionGroupDescription', "Read-only customizations provided by installed extensions."), items: [] }, - { storage: PromptsStorage.plugin, label: localize('pluginGroup', "Plugins"), icon: pluginIcon, description: localize('pluginGroupDescription', "Read-only customizations provided by installed plugins."), items: [] }, - ].filter(g => visibleSources.has(g.storage)); + const groups: { groupKey: string; label: string; icon: ThemeIcon; description: string; items: IAICustomizationListItem[] }[] = [ + { groupKey: PromptsStorage.local, label: localize('workspaceGroup', "Workspace"), icon: workspaceIcon, description: localize('workspaceGroupDescription', "Customizations stored as files in your project folder and shared with your team via version control."), items: [] }, + { groupKey: PromptsStorage.user, label: localize('userGroup', "User"), icon: userIcon, description: localize('userGroupDescription', "Customizations stored locally on your machine in a central location. Private to you and available across all projects."), items: [] }, + { groupKey: PromptsStorage.extension, label: localize('extensionGroup', "Extensions"), icon: extensionIcon, description: localize('extensionGroupDescription', "Read-only customizations provided by installed extensions."), items: [] }, + { groupKey: PromptsStorage.plugin, label: localize('pluginGroup', "Plugins"), icon: pluginIcon, description: localize('pluginGroupDescription', "Read-only customizations provided by installed plugins."), items: [] }, + { groupKey: 'agents', label: localize('agentsGroup', "Agents"), icon: agentIcon, description: localize('agentsGroupDescription', "Hooks defined in agent files."), items: [] }, + ].filter(g => visibleSources.has(g.groupKey as PromptsStorage) || g.groupKey === 'agents'); for (const item of matchedItems) { - const group = groups.find(g => g.storage === item.storage); + const key = item.groupKey ?? item.storage; + const group = groups.find(g => g.groupKey === key); if (group) { group.items.push(item); } @@ -967,12 +1007,12 @@ export class AICustomizationListWidget extends Disposable { continue; } - const collapsed = this.collapsedGroups.has(group.storage); + const collapsed = this.collapsedGroups.has(group.groupKey); this.displayEntries.push({ type: 'group-header', - id: `group-${group.storage}`, - storage: group.storage, + id: `group-${group.groupKey}`, + groupKey: group.groupKey, label: group.label, icon: group.icon, count: group.items.length, @@ -997,10 +1037,10 @@ export class AICustomizationListWidget extends Disposable { * Toggles the collapsed state of a group. */ private toggleGroup(entry: IGroupHeaderEntry): void { - if (this.collapsedGroups.has(entry.storage)) { - this.collapsedGroups.delete(entry.storage); + if (this.collapsedGroups.has(entry.groupKey)) { + this.collapsedGroups.delete(entry.groupKey); } else { - this.collapsedGroups.add(entry.storage); + this.collapsedGroups.add(entry.groupKey); } this.filterItems(); } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index e3b8f65df506b..ed1667fbed95d 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -33,6 +33,7 @@ import { PANEL_BORDER } from '../../../../common/theme.js'; import { AICustomizationManagementEditorInput } from './aiCustomizationManagementEditorInput.js'; import { AICustomizationListWidget } from './aiCustomizationListWidget.js'; import { McpListWidget } from './mcpListWidget.js'; +import { PluginListWidget } from './pluginListWidget.js'; import { AI_CUSTOMIZATION_MANAGEMENT_EDITOR_ID, AI_CUSTOMIZATION_MANAGEMENT_SIDEBAR_WIDTH_KEY, @@ -45,7 +46,7 @@ import { SIDEBAR_MAX_WIDTH, CONTENT_MIN_WIDTH, } from './aiCustomizationManagement.js'; -import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon } from './aiCustomizationIcons.js'; +import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon, pluginIcon } from './aiCustomizationIcons.js'; import { ChatModelsWidget } from '../chatManagement/chatModelsWidget.js'; import { PromptsType, Target } from '../../common/promptSyntax/promptTypes.js'; import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; @@ -66,6 +67,9 @@ import { McpServerEditorInput } from '../../../mcp/browser/mcpServerEditorInput. import { McpServerEditor } from '../../../mcp/browser/mcpServerEditor.js'; import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IWorkbenchMcpServer } from '../../../mcp/common/mcpTypes.js'; +import { AgentPluginEditor } from '../agentPluginEditor/agentPluginEditor.js'; +import { AgentPluginEditorInput } from '../agentPluginEditor/agentPluginEditorInput.js'; +import { IAgentPluginItem } from '../agentPluginEditor/agentPluginItems.js'; const $ = DOM.$; @@ -137,9 +141,11 @@ export class AICustomizationManagementEditor extends EditorPane { private contentContainer!: HTMLElement; private listWidget!: AICustomizationListWidget; private mcpListWidget: McpListWidget | undefined; + private pluginListWidget: PluginListWidget | undefined; private modelsWidget: ChatModelsWidget | undefined; private promptsContentContainer!: HTMLElement; private mcpContentContainer: HTMLElement | undefined; + private pluginContentContainer: HTMLElement | undefined; private modelsContentContainer: HTMLElement | undefined; private modelsFooterElement: HTMLElement | undefined; @@ -153,13 +159,18 @@ export class AICustomizationManagementEditor extends EditorPane { private currentEditingUri: URI | undefined; private currentEditingProjectRoot: URI | undefined; private currentModelRef: IReference | undefined; - private viewMode: 'list' | 'editor' | 'mcpDetail' = 'list'; + private viewMode: 'list' | 'editor' | 'mcpDetail' | 'pluginDetail' = 'list'; // Embedded MCP server detail view private mcpDetailContainer: HTMLElement | undefined; private embeddedMcpEditor: McpServerEditor | undefined; private readonly mcpDetailDisposables = this._register(new DisposableStore()); + // Embedded plugin detail view + private pluginDetailContainer: HTMLElement | undefined; + private embeddedPluginEditor: AgentPluginEditor | undefined; + private readonly pluginDetailDisposables = this._register(new DisposableStore()); + private dimension: DOM.Dimension | undefined; private readonly sections: ISectionItem[] = []; private selectedSection: AICustomizationManagementSection = AICustomizationManagementSection.Agents; @@ -218,6 +229,7 @@ export class AICustomizationManagementEditor extends EditorPane { [AICustomizationManagementSection.Prompts]: { label: localize('prompts', "Prompts"), icon: promptIcon }, [AICustomizationManagementSection.Hooks]: { label: localize('hooks', "Hooks"), icon: hookIcon }, [AICustomizationManagementSection.McpServers]: { label: localize('mcpServers', "MCP Servers"), icon: Codicon.server }, + [AICustomizationManagementSection.Plugins]: { label: localize('plugins', "Plugins"), icon: pluginIcon }, [AICustomizationManagementSection.Models]: { label: localize('models', "Models"), icon: Codicon.vm }, }; for (const id of this.workspaceService.managementSections) { @@ -287,6 +299,7 @@ export class AICustomizationManagementEditor extends EditorPane { if (height !== undefined) { this.listWidget.layout(height - 16, width - 24); this.mcpListWidget?.layout(height - 16, width - 24); + this.pluginListWidget?.layout(height - 16, width - 24); const modelsFooterHeight = this.modelsFooterElement?.offsetHeight || 80; this.modelsWidget?.layout(height - 16 - modelsFooterHeight, width); if (this.viewMode === 'editor' && this.embeddedEditor) { @@ -298,6 +311,10 @@ export class AICustomizationManagementEditor extends EditorPane { const backHeaderHeight = 40; this.embeddedMcpEditor.layout(new DOM.Dimension(width, Math.max(0, height - backHeaderHeight))); } + if (this.viewMode === 'pluginDetail' && this.embeddedPluginEditor) { + const backHeaderHeight = 40; + this.embeddedPluginEditor.layout(new DOM.Dimension(width, Math.max(0, height - backHeaderHeight))); + } } }, }, Sizing.Distribute, undefined, true); @@ -483,6 +500,21 @@ export class AICustomizationManagementEditor extends EditorPane { })); } + // Container for Plugins content + if (hasSections.has(AICustomizationManagementSection.Plugins)) { + this.pluginContentContainer = DOM.append(contentInner, $('.plugin-content-container')); + this.pluginListWidget = this.editorDisposables.add(this.instantiationService.createInstance(PluginListWidget)); + this.pluginContentContainer.appendChild(this.pluginListWidget.element); + + // Embedded plugin detail view + this.pluginDetailContainer = DOM.append(contentInner, $('.plugin-detail-container')); + this.createEmbeddedPluginDetail(); + + this.editorDisposables.add(this.pluginListWidget.onDidSelectPlugin(item => { + this.showEmbeddedPluginDetail(item); + })); + } + // Embedded editor container this.editorContentContainer = DOM.append(contentInner, $('.editor-content-container')); this.createEmbeddedEditor(); @@ -515,6 +547,9 @@ export class AICustomizationManagementEditor extends EditorPane { if (this.viewMode === 'mcpDetail') { this.goBackFromMcpDetail(); } + if (this.viewMode === 'pluginDetail') { + this.goBackFromPluginDetail(); + } this.selectedSection = section; this.sectionContextKey.set(section); @@ -534,20 +569,29 @@ export class AICustomizationManagementEditor extends EditorPane { private updateContentVisibility(): void { const isEditorMode = this.viewMode === 'editor'; const isMcpDetailMode = this.viewMode === 'mcpDetail'; + const isPluginDetailMode = this.viewMode === 'pluginDetail'; + const isDetailMode = isMcpDetailMode || isPluginDetailMode; const isPromptsSection = this.isPromptsSection(this.selectedSection); const isModelsSection = this.selectedSection === AICustomizationManagementSection.Models; const isMcpSection = this.selectedSection === AICustomizationManagementSection.McpServers; + const isPluginsSection = this.selectedSection === AICustomizationManagementSection.Plugins; - this.promptsContentContainer.style.display = !isEditorMode && !isMcpDetailMode && isPromptsSection ? '' : 'none'; + this.promptsContentContainer.style.display = !isEditorMode && !isDetailMode && isPromptsSection ? '' : 'none'; if (this.modelsContentContainer) { - this.modelsContentContainer.style.display = !isEditorMode && !isMcpDetailMode && isModelsSection ? '' : 'none'; + this.modelsContentContainer.style.display = !isEditorMode && !isDetailMode && isModelsSection ? '' : 'none'; } if (this.mcpContentContainer) { - this.mcpContentContainer.style.display = !isEditorMode && !isMcpDetailMode && isMcpSection ? '' : 'none'; + this.mcpContentContainer.style.display = !isEditorMode && !isDetailMode && isMcpSection ? '' : 'none'; } if (this.mcpDetailContainer) { this.mcpDetailContainer.style.display = isMcpDetailMode ? '' : 'none'; } + if (this.pluginContentContainer) { + this.pluginContentContainer.style.display = !isEditorMode && !isDetailMode && isPluginsSection ? '' : 'none'; + } + if (this.pluginDetailContainer) { + this.pluginDetailContainer.style.display = isPluginDetailMode ? '' : 'none'; + } if (this.editorContentContainer) { this.editorContentContainer.style.display = isEditorMode ? '' : 'none'; } @@ -654,6 +698,9 @@ export class AICustomizationManagementEditor extends EditorPane { if (this.viewMode === 'mcpDetail') { this.goBackFromMcpDetail(); } + if (this.viewMode === 'pluginDetail') { + this.goBackFromPluginDetail(); + } // Clear transient folder override on close this.workspaceService.clearOverrideProjectRoot(); super.clearInput(); @@ -676,6 +723,8 @@ export class AICustomizationManagementEditor extends EditorPane { } if (this.selectedSection === AICustomizationManagementSection.McpServers) { this.mcpListWidget?.focusSearch(); + } else if (this.selectedSection === AICustomizationManagementSection.Plugins) { + this.pluginListWidget?.focusSearch(); } else if (this.selectedSection === AICustomizationManagementSection.Models) { this.modelsWidget?.focusSearch(); } else { @@ -698,6 +747,9 @@ export class AICustomizationManagementEditor extends EditorPane { if (this.viewMode === 'mcpDetail') { this.goBackFromMcpDetail(); } + if (this.viewMode === 'pluginDetail') { + this.goBackFromPluginDetail(); + } this.selectedSection = sectionId; this.sectionContextKey.set(sectionId); this.storageService.store(AI_CUSTOMIZATION_MANAGEMENT_SELECTED_SECTION_KEY, sectionId, StorageScope.PROFILE, StorageTarget.USER); @@ -783,6 +835,12 @@ export class AICustomizationManagementEditor extends EditorPane { try { const ref = await this.textModelService.createModelReference(uri); + + if (!isEqual(this.currentEditingUri, uri)) { + ref.dispose(); + return; // another item was selected while loading + } + this.currentModelRef = ref; this.embeddedEditor!.setModel(ref.object.textEditorModel); this.embeddedEditor!.updateOptions({ readOnly: isReadOnly }); @@ -820,7 +878,9 @@ export class AICustomizationManagementEditor extends EditorPane { })); } catch (error) { console.error('Failed to load model for embedded editor:', error); - this.goBackToList(); + if (isEqual(this.currentEditingUri, uri)) { + this.goBackToList(); + } } } @@ -916,4 +976,67 @@ export class AICustomizationManagementEditor extends EditorPane { } //#endregion + + //#region Embedded Plugin Detail + + private createEmbeddedPluginDetail(): void { + if (!this.pluginDetailContainer) { + return; + } + + // Back button header + const detailHeader = DOM.append(this.pluginDetailContainer, $('.editor-header')); + const backButton = DOM.append(detailHeader, $('button.editor-back-button')); + backButton.setAttribute('aria-label', localize('backToPluginList', "Back to plugins")); + const backIconEl = DOM.append(backButton, $(`.codicon.codicon-${Codicon.arrowLeft.id}`)); + backIconEl.setAttribute('aria-hidden', 'true'); + this.editorDisposables.add(DOM.addDisposableListener(backButton, 'click', () => { + this.goBackFromPluginDetail(); + })); + + // Container for the plugin editor + const editorContainer = DOM.append(this.pluginDetailContainer, $('.plugin-detail-editor-container')); + + // Create the embedded plugin editor pane + this.embeddedPluginEditor = this.editorDisposables.add(this.instantiationService.createInstance(AgentPluginEditor, this.group)); + this.embeddedPluginEditor.create(editorContainer); + } + + private async showEmbeddedPluginDetail(item: IAgentPluginItem): Promise { + if (!this.embeddedPluginEditor) { + return; + } + + this.viewMode = 'pluginDetail'; + this.updateContentVisibility(); + + const input = new AgentPluginEditorInput(item); + this.pluginDetailDisposables.clear(); + this.pluginDetailDisposables.add(input); + + try { + await this.embeddedPluginEditor.setInput(input, undefined, {}, CancellationToken.None); + } catch { + this.goBackFromPluginDetail(); + return; + } + + if (this.dimension) { + this.layout(this.dimension); + } + } + + private goBackFromPluginDetail(): void { + this.pluginDetailDisposables.clear(); + this.embeddedPluginEditor?.clearInput(); + this.viewMode = 'list'; + this.updateContentVisibility(); + + if (this.dimension) { + this.layout(this.dimension); + } + this.pluginListWidget?.focusSearch(); + } + + //#endregion } diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts index 3ebfcea382f12..dea000c208606 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWorkspaceService.ts @@ -53,6 +53,7 @@ class AICustomizationWorkspaceService implements IAICustomizationWorkspaceServic AICustomizationManagementSection.Prompts, AICustomizationManagementSection.Hooks, AICustomizationManagementSection.McpServers, + AICustomizationManagementSection.Plugins, ]; private static readonly _defaultFilter: IStorageSourceFilter = { diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css index dd08e6fb3802c..d9781fd7664f5 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/media/aiCustomizationManagement.css @@ -556,6 +556,7 @@ /* Content container visibility */ .ai-customization-management-editor .prompts-content-container, .ai-customization-management-editor .mcp-content-container, +.ai-customization-management-editor .plugin-content-container, .ai-customization-management-editor .models-content-container { height: 100%; display: flex; @@ -574,6 +575,18 @@ overflow: hidden; } +/* Embedded plugin detail view */ +.ai-customization-management-editor .plugin-detail-container { + height: 100%; + display: flex; + flex-direction: column; +} + +.ai-customization-management-editor .plugin-detail-editor-container { + flex: 1; + overflow: hidden; +} + /* Models section footer */ .ai-customization-management-editor .models-content-container .section-footer { flex-shrink: 0; diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts new file mode 100644 index 0000000000000..52e409f290bd5 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/pluginListWidget.ts @@ -0,0 +1,859 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/aiCustomizationManagement.css'; +import * as DOM from '../../../../../base/browser/dom.js'; +import { Disposable, DisposableStore, isDisposable } from '../../../../../base/common/lifecycle.js'; +import { Emitter } from '../../../../../base/common/event.js'; +import { localize } from '../../../../../nls.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { WorkbenchList } from '../../../../../platform/list/browser/listService.js'; +import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent } from '../../../../../base/browser/ui/list/list.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { Button } from '../../../../../base/browser/ui/button/button.js'; +import { defaultButtonStyles, defaultInputBoxStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; +import { autorun } from '../../../../../base/common/observable.js'; +import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { InputBox } from '../../../../../base/browser/ui/inputbox/inputBox.js'; +import { IContextMenuService, IContextViewService } from '../../../../../platform/contextview/browser/contextView.js'; +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { Delayer } from '../../../../../base/common/async.js'; +import { IAction, Action, Separator } from '../../../../../base/common/actions.js'; +import { basename, dirname } from '../../../../../base/common/resources.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { IAgentPlugin, IAgentPluginService } from '../../common/plugins/agentPluginService.js'; +import { IMarketplacePlugin, IPluginMarketplaceService } from '../../common/plugins/pluginMarketplaceService.js'; +import { IPluginInstallService } from '../../common/plugins/pluginInstallService.js'; +import { AgentPluginItemKind, IAgentPluginItem, IInstalledPluginItem, IMarketplacePluginItem } from '../agentPluginEditor/agentPluginItems.js'; +import { pluginIcon } from './aiCustomizationIcons.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; + +const $ = DOM.$; + +const PLUGIN_ITEM_HEIGHT = 36; +const PLUGIN_GROUP_HEADER_HEIGHT = 36; +const PLUGIN_GROUP_HEADER_HEIGHT_WITH_SEPARATOR = 40; + +//#region Entry types + +/** + * Represents a collapsible group header in the plugin list. + */ +interface IPluginGroupHeaderEntry { + readonly type: 'group-header'; + readonly id: string; + readonly group: 'enabled' | 'disabled'; + readonly label: string; + readonly icon: ThemeIcon; + readonly count: number; + readonly isFirst: boolean; + readonly description: string; + collapsed: boolean; +} + +/** + * Represents an installed plugin item in the list. + */ +interface IPluginInstalledItemEntry { + readonly type: 'plugin-item'; + readonly item: IInstalledPluginItem; +} + +/** + * Represents a marketplace plugin item in the list (browse mode). + */ +interface IPluginMarketplaceItemEntry { + readonly type: 'marketplace-item'; + readonly item: IMarketplacePluginItem; +} + +type IPluginListEntry = IPluginGroupHeaderEntry | IPluginInstalledItemEntry | IPluginMarketplaceItemEntry; + +//#endregion + +//#region Delegate + +class PluginItemDelegate implements IListVirtualDelegate { + getHeight(element: IPluginListEntry): number { + if (element.type === 'group-header') { + return element.isFirst ? PLUGIN_GROUP_HEADER_HEIGHT : PLUGIN_GROUP_HEADER_HEIGHT_WITH_SEPARATOR; + } + return PLUGIN_ITEM_HEIGHT; + } + + getTemplateId(element: IPluginListEntry): string { + if (element.type === 'group-header') { + return 'pluginGroupHeader'; + } + if (element.type === 'marketplace-item') { + return 'pluginMarketplaceItem'; + } + return 'pluginInstalledItem'; + } +} + +//#endregion + +//#region Group Header Renderer (reuses .ai-customization-group-header CSS) + +interface IPluginGroupHeaderTemplateData { + readonly container: HTMLElement; + readonly chevron: HTMLElement; + readonly icon: HTMLElement; + readonly label: HTMLElement; + readonly count: HTMLElement; + readonly infoIcon: HTMLElement; + readonly disposables: DisposableStore; + readonly elementDisposables: DisposableStore; +} + +class PluginGroupHeaderRenderer implements IListRenderer { + readonly templateId = 'pluginGroupHeader'; + + constructor( + private readonly hoverService: IHoverService, + ) { } + + renderTemplate(container: HTMLElement): IPluginGroupHeaderTemplateData { + const disposables = new DisposableStore(); + const elementDisposables = new DisposableStore(); + container.classList.add('ai-customization-group-header'); + + const chevron = DOM.append(container, $('.group-chevron')); + const icon = DOM.append(container, $('.group-icon')); + const labelGroup = DOM.append(container, $('.group-label-group')); + const label = DOM.append(labelGroup, $('.group-label')); + const infoIcon = DOM.append(labelGroup, $('.group-info')); + infoIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.info)); + const count = DOM.append(container, $('.group-count')); + + return { container, chevron, icon, label, count, infoIcon, disposables, elementDisposables }; + } + + renderElement(element: IPluginGroupHeaderEntry, _index: number, templateData: IPluginGroupHeaderTemplateData): void { + templateData.elementDisposables.clear(); + + templateData.chevron.className = 'group-chevron'; + templateData.chevron.classList.add(...ThemeIcon.asClassNameArray(element.collapsed ? Codicon.chevronRight : Codicon.chevronDown)); + + templateData.icon.className = 'group-icon'; + templateData.icon.classList.add(...ThemeIcon.asClassNameArray(element.icon)); + + templateData.label.textContent = element.label; + templateData.count.textContent = `${element.count}`; + + templateData.elementDisposables.add(this.hoverService.setupDelayedHover(templateData.infoIcon, () => ({ + content: element.description, + appearance: { + compact: true, + skipFadeInAnimation: true, + } + }))); + + templateData.container.classList.toggle('collapsed', element.collapsed); + templateData.container.classList.toggle('has-previous-group', !element.isFirst); + } + + disposeTemplate(templateData: IPluginGroupHeaderTemplateData): void { + templateData.elementDisposables.dispose(); + templateData.disposables.dispose(); + } +} + +//#endregion + +//#region Installed Plugin Renderer (reuses .mcp-server-item CSS) + +interface IPluginInstalledItemTemplateData { + readonly container: HTMLElement; + readonly name: HTMLElement; + readonly description: HTMLElement; + readonly status: HTMLElement; + readonly disposables: DisposableStore; +} + +class PluginInstalledItemRenderer implements IListRenderer { + readonly templateId = 'pluginInstalledItem'; + + renderTemplate(container: HTMLElement): IPluginInstalledItemTemplateData { + container.classList.add('mcp-server-item'); + + const details = DOM.append(container, $('.mcp-server-details')); + const name = DOM.append(details, $('.mcp-server-name')); + const description = DOM.append(details, $('.mcp-server-description')); + const status = DOM.append(container, $('.mcp-server-status')); + + return { container, name, description, status, disposables: new DisposableStore() }; + } + + renderElement(element: IPluginInstalledItemEntry, _index: number, templateData: IPluginInstalledItemTemplateData): void { + templateData.disposables.clear(); + + templateData.name.textContent = element.item.name; + + if (element.item.description) { + templateData.description.textContent = element.item.description; + templateData.description.style.display = ''; + } else { + templateData.description.style.display = 'none'; + } + + // Show enabled/disabled status + templateData.disposables.add(autorun(reader => { + const enabled = element.item.plugin.enabled.read(reader); + templateData.status.className = 'mcp-server-status'; + if (enabled) { + templateData.status.textContent = localize('enabled', "Enabled"); + templateData.status.classList.add('running'); + } else { + templateData.status.textContent = localize('disabled', "Disabled"); + templateData.status.classList.add('stopped'); + } + })); + } + + disposeTemplate(templateData: IPluginInstalledItemTemplateData): void { + templateData.disposables.dispose(); + } +} + +//#endregion + +//#region Marketplace Plugin Renderer (reuses .mcp-gallery-item CSS) + +interface IPluginMarketplaceItemTemplateData { + readonly container: HTMLElement; + readonly name: HTMLElement; + readonly publisher: HTMLElement; + readonly description: HTMLElement; + readonly installButton: Button; + readonly elementDisposables: DisposableStore; + readonly templateDisposables: DisposableStore; +} + +class PluginMarketplaceItemRenderer implements IListRenderer { + readonly templateId = 'pluginMarketplaceItem'; + + constructor( + private readonly pluginInstallService: IPluginInstallService, + ) { } + + renderTemplate(container: HTMLElement): IPluginMarketplaceItemTemplateData { + container.classList.add('mcp-server-item', 'mcp-gallery-item'); + + const details = DOM.append(container, $('.mcp-server-details')); + const nameRow = DOM.append(details, $('.mcp-gallery-name-row')); + const name = DOM.append(nameRow, $('.mcp-server-name')); + const publisher = DOM.append(nameRow, $('.mcp-gallery-publisher')); + const description = DOM.append(details, $('.mcp-server-description')); + + const actionContainer = DOM.append(container, $('.mcp-gallery-action')); + const installButton = new Button(actionContainer, { ...defaultButtonStyles, supportIcons: true }); + installButton.element.classList.add('mcp-gallery-install-button'); + + const templateDisposables = new DisposableStore(); + templateDisposables.add(installButton); + + return { container, name, publisher, description, installButton, elementDisposables: new DisposableStore(), templateDisposables }; + } + + renderElement(element: IPluginMarketplaceItemEntry, _index: number, templateData: IPluginMarketplaceItemTemplateData): void { + templateData.elementDisposables.clear(); + + templateData.name.textContent = element.item.name; + templateData.publisher.textContent = element.item.marketplace ? localize('byPublisher', "by {0}", element.item.marketplace) : ''; + templateData.description.textContent = element.item.description || ''; + + templateData.installButton.label = localize('install', "Install"); + templateData.installButton.enabled = true; + + templateData.elementDisposables.add(templateData.installButton.onDidClick(async () => { + templateData.installButton.label = localize('installing', "Installing..."); + templateData.installButton.enabled = false; + try { + await this.pluginInstallService.installPlugin({ + name: element.item.name, + description: element.item.description, + version: '', + sourceDescriptor: element.item.sourceDescriptor, + source: element.item.source, + marketplace: element.item.marketplace, + marketplaceReference: element.item.marketplaceReference, + marketplaceType: element.item.marketplaceType, + readmeUri: element.item.readmeUri, + }); + templateData.installButton.label = localize('installed', "Installed"); + } catch (_e) { + templateData.installButton.label = localize('install', "Install"); + templateData.installButton.enabled = true; + } + })); + } + + disposeTemplate(templateData: IPluginMarketplaceItemTemplateData): void { + templateData.elementDisposables.dispose(); + templateData.templateDisposables.dispose(); + } +} + +//#endregion + +//#region Plugin context menu actions + +function getInstalledPluginContextMenuActions(plugin: IAgentPlugin, instantiationService: IInstantiationService): IAction[][] { + const groups: IAction[][] = []; + if (plugin.enabled.get()) { + groups.push([instantiationService.createInstance(DisablePluginAction, plugin)]); + } else { + groups.push([instantiationService.createInstance(EnablePluginAction, plugin)]); + } + groups.push([ + instantiationService.createInstance(OpenPluginFolderAction, plugin), + ]); + if (plugin.fromMarketplace) { + groups.push([new UninstallPluginAction(plugin)]); + } + return groups; +} + +class EnablePluginAction extends Action { + constructor( + private readonly plugin: IAgentPlugin, + @IAgentPluginService private readonly agentPluginService: IAgentPluginService, + ) { + super('pluginListWidget.enable', localize('enable', "Enable")); + } + + override async run(): Promise { + this.agentPluginService.setPluginEnabled(this.plugin.uri, true); + } +} + +class DisablePluginAction extends Action { + constructor( + private readonly plugin: IAgentPlugin, + @IAgentPluginService private readonly agentPluginService: IAgentPluginService, + ) { + super('pluginListWidget.disable', localize('disable', "Disable")); + } + + override async run(): Promise { + this.agentPluginService.setPluginEnabled(this.plugin.uri, false); + } +} + +class OpenPluginFolderAction extends Action { + constructor( + private readonly plugin: IAgentPlugin, + @ICommandService private readonly commandService: ICommandService, + @IOpenerService private readonly openerService: IOpenerService, + ) { + super('pluginListWidget.openFolder', localize('openPluginFolder', "Open Plugin Folder")); + } + + override async run(): Promise { + try { + await this.commandService.executeCommand('revealFileInOS', this.plugin.uri); + } catch { + await this.openerService.open(dirname(this.plugin.uri)); + } + } +} + +class UninstallPluginAction extends Action { + constructor( + private readonly plugin: IAgentPlugin, + ) { + super('pluginListWidget.uninstall', localize('uninstall', "Uninstall")); + } + + override async run(): Promise { + this.plugin.remove(); + } +} + +//#endregion + +//#region Helpers + +function installedPluginToItem(plugin: IAgentPlugin, labelService: ILabelService): IInstalledPluginItem { + const name = plugin.label ?? basename(plugin.uri); + const description = plugin.fromMarketplace?.description ?? labelService.getUriLabel(dirname(plugin.uri), { relative: true }); + const marketplace = plugin.fromMarketplace?.marketplace; + return { kind: AgentPluginItemKind.Installed, name, description, marketplace, plugin }; +} + +function marketplacePluginToItem(plugin: IMarketplacePlugin): IMarketplacePluginItem { + return { + kind: AgentPluginItemKind.Marketplace, + name: plugin.name, + description: plugin.description, + source: plugin.source, + sourceDescriptor: plugin.sourceDescriptor, + marketplace: plugin.marketplace, + marketplaceReference: plugin.marketplaceReference, + marketplaceType: plugin.marketplaceType, + readmeUri: plugin.readmeUri, + }; +} + +//#endregion + +/** + * Widget that displays a list of agent plugins with marketplace browsing. + * Follows the same patterns as {@link McpListWidget}. + */ +export class PluginListWidget extends Disposable { + + readonly element: HTMLElement; + + private readonly _onDidSelectPlugin = this._register(new Emitter()); + readonly onDidSelectPlugin = this._onDidSelectPlugin.event; + + private sectionHeader!: HTMLElement; + private sectionDescription!: HTMLElement; + private sectionLink!: HTMLAnchorElement; + private searchAndButtonContainer!: HTMLElement; + private searchInput!: InputBox; + private listContainer!: HTMLElement; + private list!: WorkbenchList; + private emptyContainer!: HTMLElement; + private emptyText!: HTMLElement; + private emptySubtext!: HTMLElement; + private browseButton!: Button; + private backLink!: HTMLElement; + + private installedItems: IInstalledPluginItem[] = []; + private displayEntries: IPluginListEntry[] = []; + private marketplaceItems: IMarketplacePluginItem[] = []; + private searchQuery: string = ''; + private browseMode: boolean = false; + private readonly collapsedGroups = new Set(); + private marketplaceCts: CancellationTokenSource | undefined; + private readonly delayedFilter = new Delayer(200); + private readonly delayedMarketplaceSearch = new Delayer(400); + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IAgentPluginService private readonly agentPluginService: IAgentPluginService, + @IPluginMarketplaceService private readonly pluginMarketplaceService: IPluginMarketplaceService, + @IPluginInstallService private readonly pluginInstallService: IPluginInstallService, + @IOpenerService private readonly openerService: IOpenerService, + @IContextViewService private readonly contextViewService: IContextViewService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IHoverService private readonly hoverService: IHoverService, + @ILabelService private readonly labelService: ILabelService, + ) { + super(); + this.element = $('.mcp-list-widget'); // reuse MCP list widget CSS + this.create(); + this._register({ + dispose: () => { + this.marketplaceCts?.dispose(); + } + }); + } + + private create(): void { + // Search and button container + this.searchAndButtonContainer = DOM.append(this.element, $('.list-search-and-button-container')); + + // Search container + const searchContainer = DOM.append(this.searchAndButtonContainer, $('.list-search-container')); + this.searchInput = this._register(new InputBox(searchContainer, this.contextViewService, { + placeholder: localize('searchPluginsPlaceholder', "Type to search..."), + inputBoxStyles: defaultInputBoxStyles, + })); + + this._register(this.searchInput.onDidChange(() => { + this.searchQuery = this.searchInput.value; + if (this.browseMode) { + this.delayedMarketplaceSearch.trigger(() => this.queryMarketplace()); + } else { + this.delayedFilter.trigger(() => this.filterPlugins()); + } + })); + + // Button container (Browse Marketplace) + const buttonContainer = DOM.append(this.searchAndButtonContainer, $('.list-button-group')); + + const browseButtonContainer = DOM.append(buttonContainer, $('.list-add-button-container')); + this.browseButton = this._register(new Button(browseButtonContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); + this.browseButton.label = `$(${Codicon.library.id}) ${localize('browseMarketplace', "Browse Marketplace")}`; + this.browseButton.element.classList.add('list-add-button'); + this._register(this.browseButton.onDidClick(() => { + this.toggleBrowseMode(!this.browseMode); + })); + + // Back to installed link (shown only in browse mode) + this.backLink = DOM.append(this.element, $('.mcp-back-link')); + this.backLink.setAttribute('role', 'button'); + this.backLink.tabIndex = 0; + this.backLink.setAttribute('aria-label', localize('backToInstalledPluginsAriaLabel', "Back to installed plugins")); + const backIcon = DOM.append(this.backLink, $('span')); + backIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.arrowLeft)); + const backText = DOM.append(this.backLink, $('span')); + backText.textContent = localize('backToInstalledPlugins', "Back to installed plugins"); + this._register(DOM.addDisposableListener(this.backLink, 'click', () => { + this.toggleBrowseMode(false); + })); + this._register(DOM.addDisposableListener(this.backLink, 'keydown', (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.toggleBrowseMode(false); + } + })); + this.backLink.style.display = 'none'; + + // Empty state + this.emptyContainer = DOM.append(this.element, $('.mcp-empty-state')); + const emptyIcon = DOM.append(this.emptyContainer, $('.empty-icon')); + emptyIcon.classList.add(...ThemeIcon.asClassNameArray(pluginIcon)); + this.emptyText = DOM.append(this.emptyContainer, $('.empty-text')); + this.emptySubtext = DOM.append(this.emptyContainer, $('.empty-subtext')); + + // List container + this.listContainer = DOM.append(this.element, $('.mcp-list-container')); + + // Section footer + this.sectionHeader = DOM.append(this.element, $('.section-footer')); + this.sectionDescription = DOM.append(this.sectionHeader, $('p.section-footer-description')); + this.sectionDescription.textContent = localize('pluginsDescription', "Extend your AI agent with plugins that add commands, skills, agents, hooks, and MCP servers from reusable packages."); + this.sectionLink = DOM.append(this.sectionHeader, $('a.section-footer-link')) as HTMLAnchorElement; + this.sectionLink.textContent = localize('learnMorePlugins', "Learn more about agent plugins"); + this.sectionLink.href = 'https://code.visualstudio.com/docs/copilot/chat/agent-plugins'; + this._register(DOM.addDisposableListener(this.sectionLink, 'click', (e) => { + e.preventDefault(); + const href = this.sectionLink.href; + if (href) { + this.openerService.open(URI.parse(href)); + } + })); + + // Create list + const delegate = new PluginItemDelegate(); + const groupHeaderRenderer = new PluginGroupHeaderRenderer(this.hoverService); + const installedRenderer = new PluginInstalledItemRenderer(); + const marketplaceRenderer = new PluginMarketplaceItemRenderer(this.pluginInstallService); + + this.list = this._register(this.instantiationService.createInstance( + WorkbenchList, + 'PluginManagementList', + this.listContainer, + delegate, + [groupHeaderRenderer, installedRenderer, marketplaceRenderer], + { + multipleSelectionSupport: false, + setRowLineHeight: false, + horizontalScrolling: false, + accessibilityProvider: { + getAriaLabel(element: IPluginListEntry) { + if (element.type === 'group-header') { + return localize('pluginGroupAriaLabel', "{0}, {1} items, {2}", element.label, element.count, element.collapsed ? localize('collapsed', "collapsed") : localize('expanded', "expanded")); + } + if (element.type === 'marketplace-item') { + return element.item.name; + } + return element.item.name; + }, + getWidgetAriaLabel() { + return localize('pluginsListAriaLabel', "Plugins"); + } + }, + openOnSingleClick: true, + identityProvider: { + getId(element: IPluginListEntry) { + if (element.type === 'group-header') { + return element.id; + } + if (element.type === 'marketplace-item') { + return `marketplace-${element.item.marketplaceReference.canonicalId}/${element.item.source}`; + } + return element.item.plugin.uri.toString(); + } + } + } + )); + + this._register(this.list.onDidOpen(e => { + if (e.element) { + if (e.element.type === 'group-header') { + this.toggleGroup(e.element); + } else if (e.element.type === 'plugin-item') { + this._onDidSelectPlugin.fire(e.element.item); + } else if (e.element.type === 'marketplace-item') { + this._onDidSelectPlugin.fire(e.element.item); + } + } + })); + + // Handle context menu + this._register(this.list.onContextMenu(e => this.onContextMenu(e as IListContextMenuEvent))); + + // Listen to plugin service changes + this._register(autorun(reader => { + this.agentPluginService.allPlugins.read(reader); + if (!this.browseMode) { + this.refresh(); + } + })); + this._register(this.pluginMarketplaceService.onDidChangeMarketplaces(() => { + if (!this.browseMode) { + this.refresh(); + } + })); + + // Initial refresh + void this.refresh(); + } + + private async refresh(): Promise { + if (this.browseMode) { + await this.queryMarketplace(); + } else { + this.filterPlugins(); + } + } + + private toggleBrowseMode(browse: boolean): void { + this.browseMode = browse; + this.searchInput.value = ''; + this.searchQuery = ''; + + this.backLink.style.display = browse ? '' : 'none'; + this.browseButton.element.parentElement!.style.display = browse ? 'none' : ''; + + this.searchInput.setPlaceHolder(browse + ? localize('searchMarketplacePlaceholder', "Search plugin marketplace...") + : localize('searchPluginsPlaceholder', "Type to search...") + ); + + if (browse) { + void this.queryMarketplace(); + } else { + this.marketplaceCts?.dispose(true); + this.marketplaceItems = []; + this.filterPlugins(); + } + } + + private async queryMarketplace(): Promise { + this.marketplaceCts?.dispose(true); + const cts = this.marketplaceCts = new CancellationTokenSource(); + + // Show loading state + this.emptyContainer.style.display = 'flex'; + this.listContainer.style.display = 'none'; + this.emptyText.textContent = localize('loadingMarketplace', "Loading marketplace..."); + this.emptySubtext.textContent = ''; + + try { + const plugins = await this.pluginMarketplaceService.fetchMarketplacePlugins(cts.token); + + if (cts.token.isCancellationRequested) { + return; + } + + const query = this.searchQuery.toLowerCase().trim(); + const filtered = query + ? plugins.filter(p => p.name.toLowerCase().includes(query) || p.description.toLowerCase().includes(query)) + : plugins; + + // Filter out already-installed plugins + const installedUris = new Set(this.agentPluginService.allPlugins.get().map(p => p.uri.toString())); + this.marketplaceItems = filtered + .filter(p => { + const expectedUri = this.pluginInstallService.getPluginInstallUri(p); + return !installedUris.has(expectedUri.toString()); + }) + .map(marketplacePluginToItem); + + this.updateMarketplaceList(); + } catch { + if (!cts.token.isCancellationRequested) { + this.marketplaceItems = []; + this.emptyContainer.style.display = 'flex'; + this.listContainer.style.display = 'none'; + this.emptyText.textContent = localize('marketplaceError', "Unable to load marketplace"); + this.emptySubtext.textContent = localize('tryAgainLater', "Check your connection and try again"); + } + } + } + + private updateMarketplaceList(): void { + if (this.marketplaceItems.length === 0) { + this.emptyContainer.style.display = 'flex'; + this.listContainer.style.display = 'none'; + if (this.searchQuery.trim()) { + this.emptyText.textContent = localize('noMarketplaceResults', "No plugins match '{0}'", this.searchQuery); + this.emptySubtext.textContent = localize('tryDifferentSearch', "Try a different search term"); + } else { + this.emptyText.textContent = localize('emptyMarketplace', "No plugins available"); + this.emptySubtext.textContent = ''; + } + } else { + this.emptyContainer.style.display = 'none'; + this.listContainer.style.display = ''; + } + + const entries: IPluginListEntry[] = this.marketplaceItems.map(item => ({ type: 'marketplace-item' as const, item })); + this.list.splice(0, this.list.length, entries); + } + + private filterPlugins(): void { + const query = this.searchQuery.toLowerCase().trim(); + const allPlugins = this.agentPluginService.allPlugins.get(); + + this.installedItems = allPlugins + .map(p => installedPluginToItem(p, this.labelService)) + .filter(item => !query || + item.name.toLowerCase().includes(query) || + item.description.toLowerCase().includes(query) + ); + + if (this.installedItems.length === 0) { + this.emptyContainer.style.display = 'flex'; + this.listContainer.style.display = 'none'; + + if (this.searchQuery.trim()) { + this.emptyText.textContent = localize('noMatchingPlugins', "No plugins match '{0}'", this.searchQuery); + this.emptySubtext.textContent = localize('tryDifferentSearch', "Try a different search term"); + } else { + this.emptyText.textContent = localize('noPlugins', "No plugins installed"); + this.emptySubtext.textContent = localize('browseToAdd', "Browse the marketplace to discover and install plugins"); + } + } else { + this.emptyContainer.style.display = 'none'; + this.listContainer.style.display = ''; + } + + // Group plugins: enabled vs disabled + const enabledPlugins = this.installedItems.filter(item => item.plugin.enabled.get()); + const disabledPlugins = this.installedItems.filter(item => !item.plugin.enabled.get()); + + const entries: IPluginListEntry[] = []; + let isFirst = true; + + if (enabledPlugins.length > 0) { + const collapsed = this.collapsedGroups.has('enabled'); + entries.push({ + type: 'group-header', + id: 'plugin-group-enabled', + group: 'enabled', + label: localize('enabledGroup', "Enabled"), + icon: pluginIcon, + count: enabledPlugins.length, + isFirst, + description: localize('enabledGroupDescription', "Plugins that are currently active and providing commands, skills, agents, and other capabilities."), + collapsed, + }); + if (!collapsed) { + for (const item of enabledPlugins) { + entries.push({ type: 'plugin-item', item }); + } + } + isFirst = false; + } + + if (disabledPlugins.length > 0) { + const collapsed = this.collapsedGroups.has('disabled'); + entries.push({ + type: 'group-header', + id: 'plugin-group-disabled', + group: 'disabled', + label: localize('disabledGroup', "Disabled"), + icon: pluginIcon, + count: disabledPlugins.length, + isFirst, + description: localize('disabledGroupDescription', "Plugins that are installed but currently disabled. Enable them to use their capabilities."), + collapsed, + }); + if (!collapsed) { + for (const item of disabledPlugins) { + entries.push({ type: 'plugin-item', item }); + } + } + } + + this.displayEntries = entries; + this.list.splice(0, this.list.length, this.displayEntries); + } + + private toggleGroup(entry: IPluginGroupHeaderEntry): void { + if (this.collapsedGroups.has(entry.group)) { + this.collapsedGroups.delete(entry.group); + } else { + this.collapsedGroups.add(entry.group); + } + this.filterPlugins(); + } + + layout(height: number, width: number): void { + const sectionFooterHeight = this.sectionHeader.offsetHeight || 0; + const searchBarHeight = this.searchAndButtonContainer.offsetHeight || 40; + const backLinkHeight = this.browseMode ? (this.backLink.offsetHeight || 28) : 0; + const listHeight = height - sectionFooterHeight - searchBarHeight - backLinkHeight; + + this.listContainer.style.height = `${Math.max(0, listHeight)}px`; + this.list.layout(Math.max(0, listHeight), width); + + if (sectionFooterHeight === 0) { + DOM.getWindow(this.listContainer).requestAnimationFrame(() => { + if (this._store.isDisposed) { + return; + } + const actualFooterHeight = this.sectionHeader.offsetHeight; + if (actualFooterHeight > 0) { + const correctedHeight = height - actualFooterHeight - searchBarHeight - backLinkHeight; + this.listContainer.style.height = `${Math.max(0, correctedHeight)}px`; + this.list.layout(Math.max(0, correctedHeight), width); + } + }); + } + } + + focusSearch(): void { + this.searchInput.focus(); + } + + focus(): void { + this.list.domFocus(); + if (this.list.length > 0) { + this.list.setFocus([0]); + } + } + + private onContextMenu(e: IListContextMenuEvent): void { + if (!e.element || e.element.type !== 'plugin-item') { + return; + } + + const entry = e.element; + const disposables = new DisposableStore(); + const groups: IAction[][] = getInstalledPluginContextMenuActions(entry.item.plugin, this.instantiationService); + const actions: IAction[] = []; + for (const menuActions of groups) { + for (const menuAction of menuActions) { + actions.push(menuAction); + if (isDisposable(menuAction)) { + disposables.add(menuAction); + } + } + actions.push(new Separator()); + } + if (actions.length > 0 && actions[actions.length - 1] instanceof Separator) { + actions.pop(); + } + + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => actions, + onHide: () => disposables.dispose() + }); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 954b313b84a88..2c38f3c9874da 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -371,6 +371,12 @@ configurationRegistry.registerConfiguration({ scope: ConfigurationScope.APPLICATION_MACHINE, tags: ['experimental', 'advanced'], }, + [ChatConfiguration.AutopilotEnabled]: { + type: 'boolean', + markdownDescription: nls.localize('chat.autopilot.enabled', "Controls whether the Autopilot mode is available in the permissions picker. When enabled, Autopilot auto-approves all tool calls and continues until the task is done."), + default: true, + tags: ['experimental'], + }, [ChatConfiguration.GlobalAutoApprove]: { default: false, markdownDescription: globalAutoApproveDescription.value, diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 7fa8e87b01e28..9b3fc446d6c13 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -289,6 +289,12 @@ export interface IChatWidgetViewOptions { * redirect to a different workspace rather than executing locally. */ submitHandler?: (query: string, mode: ChatModeKind) => Promise; + + /** + * Whether we are running in the sessions window. + * When true, the secondary toolbar (permissions picker) is hidden. + */ + isSessionsWindow?: boolean; } export interface IChatViewViewContext { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css index 0c26b35ff4933..fc44d44f42fad 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css @@ -8,7 +8,7 @@ color: var(--vscode-foreground); background-color: var(--vscode-editorWidget-background); border-radius: 6px; - border: 1px solid var(--vscode-contrastBorder); + border: 1px solid var(--vscode-editorWidget-border); display: flex; align-items: center; justify-content: center; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css index e24c87525bf8b..5e5b64f1fcdcb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css @@ -21,7 +21,7 @@ border-radius: 6px; background-color: var(--vscode-editorWidget-background); color: var(--vscode-foreground); - border: 1px solid var(--vscode-contrastBorder); + border: 1px solid var(--vscode-editorWidget-border); overflow: hidden; } diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index 967ae4f95511a..309e48a4472d0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -407,7 +407,9 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } private registerContribution(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): IDisposable { + this._logService.info(`[ChatSessionsService] registerContribution called for type='${contribution.type}', canDelegate=${contribution.canDelegate}, when='${contribution.when}', extension='${ext.identifier.value}'`); if (this._contributions.has(contribution.type)) { + this._logService.info(`[ChatSessionsService] registerContribution: type='${contribution.type}' already registered, skipping`); return { dispose: () => { } }; } @@ -643,6 +645,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ for (const { contribution, extension } of this._contributions.values()) { const isCurrentlyRegistered = this._contributionDisposables.has(contribution.type); const shouldBeRegistered = this._isContributionAvailable(contribution); + this._logService.trace(`[ChatSessionsService] _evaluateAvailability: type='${contribution.type}', isCurrentlyRegistered=${isCurrentlyRegistered}, shouldBeRegistered=${shouldBeRegistered}, when='${contribution.when}'`); if (isCurrentlyRegistered && !shouldBeRegistered) { // Disable the contribution by disposing its disposable store this._contributionDisposables.deleteAndDispose(contribution.type); @@ -669,6 +672,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } private _enableContribution(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): void { + this._logService.info(`[ChatSessionsService] _enableContribution: type='${contribution.type}', canDelegate=${contribution.canDelegate}`); const disposableStore = new DisposableStore(); this._contributionDisposables.set(contribution.type, disposableStore); if (contribution.canDelegate) { @@ -1052,14 +1056,14 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ options: newSessionOptions ?? {}, dispose: () => { } }; - - for (const [optionId, value] of Object.entries(newSessionOptions ?? {})) { - this.setSessionOption(sessionResource, optionId, value); - } } else { session = await raceCancellationError(provider.provideChatSessionContent(sessionResource, token), token); } + for (const [optionId, value] of Object.entries(session.options ?? {})) { + this.setSessionOption(sessionResource, optionId, value); + } + // Make sure another session wasn't created while we were awaiting the provider { const existingSessionData = this._sessions.get(sessionResource); diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts index 805c977bf06e5..244bf9498ef52 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookActions.ts @@ -24,14 +24,14 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { IQuickInputButton, IQuickInputService, IQuickPick, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { HOOK_METADATA, HOOKS_BY_TARGET, HookType, IHookTypeMeta } from '../../common/promptSyntax/hookTypes.js'; -import { getEffectiveCommandFieldKey } from '../../common/promptSyntax/hookSchema.js'; +import { formatHookCommandLabel, getEffectiveCommandFieldKey } from '../../common/promptSyntax/hookSchema.js'; import { getCopilotCliHookTypeName, resolveCopilotCliHookType } from '../../common/promptSyntax/hookCopilotCliCompat.js'; import { getHookSourceFormat, HookSourceFormat, buildNewHookEntry } from '../../common/promptSyntax/hookCompatibility.js'; import { getClaudeHookTypeName, resolveClaudeHookType } from '../../common/promptSyntax/hookClaudeCompat.js'; import { ILabelService } from '../../../../../platform/label/common/label.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ITextEditorSelection } from '../../../../../platform/editor/common/editor.js'; -import { findHookCommandSelection, parseAllHookFiles, IParsedHook } from './hookUtils.js'; +import { findHookCommandSelection, findHookCommandInYaml, parseAllHookFiles, IParsedHook } from './hookUtils.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IPathService } from '../../../../services/path/common/pathService.js'; import { INotificationService } from '../../../../../platform/notification/common/notification.js'; @@ -348,7 +348,8 @@ export async function showConfigureHooksQuickPick( workspaceRootUri, userHome, targetOS, - CancellationToken.None + CancellationToken.None, + { includeAgentHooks: true } ); // Count hooks per type @@ -445,6 +446,10 @@ export async function showConfigureHooksQuickPick( // Filter hooks by the selected type const hooksOfType = hookEntries.filter(h => h.hookType === selectedHookType!.hookType); + // Separate hooks by source + const fileHooks = hooksOfType.filter(h => !h.agentName); + const agentHooks = hooksOfType.filter(h => h.agentName); + // Step 2: Show "Add new hook" + existing hooks of this type const hookItems: (IHookQuickPickItem | IQuickPickSeparator)[] = []; @@ -455,14 +460,14 @@ export async function showConfigureHooksQuickPick( alwaysShow: true }); - // Add existing hooks - if (hooksOfType.length > 0) { + // Add existing file-based hooks + if (fileHooks.length > 0) { hookItems.push({ type: 'separator', label: localize('existingHooks', "Existing Hooks") }); - for (const entry of hooksOfType) { + for (const entry of fileHooks) { const description = labelService.getUriLabel(entry.fileUri, { relative: true }); hookItems.push({ label: entry.commandLabel, @@ -472,6 +477,26 @@ export async function showConfigureHooksQuickPick( } } + // Add agent-defined hooks grouped by agent name + if (agentHooks.length > 0) { + const agentNames = [...new Set(agentHooks.map(h => h.agentName!))]; + for (const agentName of agentNames) { + hookItems.push({ + type: 'separator', + label: localize('agentHooks', "Agent: {0}", agentName) + }); + + for (const entry of agentHooks.filter(h => h.agentName === agentName)) { + const description = labelService.getUriLabel(entry.fileUri, { relative: true }); + hookItems.push({ + label: entry.commandLabel, + description, + hookEntry: entry + }); + } + } + } + // Auto-execute if only "Add new hook" is available (no existing hooks) if (hooksOfType.length === 0) { selectedHook = hookItems[0] as IHookQuickPickItem; @@ -500,22 +525,34 @@ export async function showConfigureHooksQuickPick( const entry = selectedHook.hookEntry; let selection: ITextEditorSelection | undefined; - // Determine the command field name to highlight based on target platform - const commandFieldName = getEffectiveCommandFieldKey(entry.command, targetOS); - - // Try to find the command field to highlight - if (commandFieldName) { + if (entry.agentName) { + // Agent hook: search the YAML frontmatter for the command try { const content = await fileService.readFile(entry.fileUri); - selection = findHookCommandSelection( - content.value.toString(), - entry.originalHookTypeId, - entry.index, - commandFieldName - ); + const commandText = formatHookCommandLabel(entry.command, targetOS); + if (commandText) { + selection = findHookCommandInYaml(content.value.toString(), commandText); + } } catch { // Ignore errors and just open without selection } + } else { + // File hook: use JSON-based selection finder + const commandFieldName = getEffectiveCommandFieldKey(entry.command, targetOS); + + if (commandFieldName) { + try { + const content = await fileService.readFile(entry.fileUri); + selection = findHookCommandSelection( + content.value.toString(), + entry.originalHookTypeId, + entry.index, + commandFieldName + ); + } catch { + // Ignore errors and just open without selection + } + } } if (options?.openEditor) { diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts index e5eac07cdb7a4..e019aecb93f25 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/hookUtils.ts @@ -114,6 +114,54 @@ export function findHookCommandSelection(content: string, hookType: string, inde }; } +/** + * Finds the selection range for a hook command string in a YAML/Markdown file + * (e.g., an agent `.md` file with YAML frontmatter). + * + * Searches for the command text within command field lines and selects the value. + * Supports all hook command field keys: command, windows, linux, osx, bash, powershell. + * + * @param content The full file content + * @param commandText The command string to locate + * @returns The selection range, or undefined if not found + */ +export function findHookCommandInYaml(content: string, commandText: string): ITextEditorSelection | undefined { + const commandFieldKeys = ['command', 'windows', 'linux', 'osx', 'bash', 'powershell']; + const lines = content.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmed = line.trimStart(); + + // Only match lines whose YAML key is a known command field + const matchedKey = commandFieldKeys.find(key => + trimmed.startsWith(`${key}:`) || trimmed.startsWith(`- ${key}:`) + ); + if (!matchedKey) { + continue; + } + + // Search after the colon to avoid matching within the key name itself + const colonIdx = line.indexOf(':'); + const idx = line.indexOf(commandText, colonIdx + 1); + if (idx !== -1) { + // Verify this is a full match (not a substring of a longer command) + const afterIdx = idx + commandText.length; + const charAfter = afterIdx < line.length ? line.charCodeAt(afterIdx) : -1; + // Accept if what follows is end of line, a quote, or whitespace + if (charAfter === -1 || charAfter === 34 /* " */ || charAfter === 39 /* ' */ || charAfter === 32 /* space */ || charAfter === 9 /* tab */) { + return { + startLineNumber: i + 1, + startColumn: idx + 1, + endLineNumber: i + 1, + endColumn: idx + 1 + commandText.length + }; + } + } + } + + return undefined; +} + /** * Parsed hook information. */ @@ -129,11 +177,15 @@ export interface IParsedHook { originalHookTypeId: string; /** If true, this hook is disabled via `disableAllHooks: true` in its file */ disabled?: boolean; + /** If set, this hook came from a custom agent's frontmatter */ + agentName?: string; } export interface IParseAllHookFilesOptions { /** Additional file URIs to parse (e.g., files skipped due to disableAllHooks) */ additionalDisabledFileUris?: readonly URI[]; + /** If true, also collect hooks from custom agent frontmatter */ + includeAgentHooks?: boolean; } /** @@ -227,5 +279,40 @@ export async function parseAllHookFiles( } } + // Collect hooks from custom agents' frontmatter + if (options?.includeAgentHooks) { + const agents = await promptsService.getCustomAgents(token); + for (const agent of agents) { + if (!agent.hooks) { + continue; + } + for (const hookTypeValue of Object.values(HookType)) { + const commands = agent.hooks[hookTypeValue]; + if (!commands || commands.length === 0) { + continue; + } + const hookTypeMeta = HOOK_METADATA[hookTypeValue]; + if (!hookTypeMeta) { + continue; + } + for (let i = 0; i < commands.length; i++) { + const command = commands[i]; + const commandLabel = formatHookCommandLabel(command, os) || nls.localize('commands.hook.emptyCommand', '(empty command)'); + parsedHooks.push({ + hookType: hookTypeValue, + hookTypeLabel: hookTypeMeta.label, + command, + commandLabel, + fileUri: agent.uri, + filePath: labelService.getUriLabel(agent.uri, { relative: true }), + index: i, + originalHookTypeId: hookTypeValue, + agentName: agent.name, + }); + } + } + } + } + return parsedHooks; } diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 13eb89e07cbaf..d9bc284672ed7 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -40,7 +40,7 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ChatRequestToolReferenceEntry, toToolSetVariableEntry, toToolVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { IVariableReference } from '../../common/chatModes.js'; import { ConfirmedReason, IChatService, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService/chatService.js'; -import { ChatConfiguration } from '../../common/constants.js'; +import { ChatConfiguration, isAutoApproveLevel } from '../../common/constants.js'; import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { IChatModel, IChatRequestModel } from '../../common/model/chatModel.js'; import { ChatToolInvocation } from '../../common/model/chatProgressTypes/chatToolInvocation.js'; @@ -641,7 +641,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo this.ensureToolDetails(dto, toolResult, tool.data); const afterExecuteState = await toolInvocation?.didExecuteTool(toolResult, undefined, () => - this.shouldAutoConfirmPostExecution(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters, dto.context?.sessionResource)); + this.shouldAutoConfirmPostExecution(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters, dto.context?.sessionResource, dto.chatRequestId)); if (toolInvocation && afterExecuteState?.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { const postConfirm = await IChatToolInvocation.awaitPostConfirmation(toolInvocation, token); @@ -791,7 +791,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } // No hook decision - use normal auto-confirm logic - const autoConfirmed = await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters, sessionResource); + const autoConfirmed = await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters, sessionResource, dto.chatRequestId); return { autoConfirmed, preparedInvocation }; } @@ -996,6 +996,15 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo }); } + /** + * Returns true if enterprise policy has explicitly disabled the global auto-approve setting. + * When this is the case, Bypass Approvals and Autopilot permission levels should not auto-approve tools. + */ + private _isAutoApprovePolicyRestricted(): boolean { + const inspected = this._configurationService.inspect(ChatConfiguration.GlobalAutoApprove); + return inspected.policyValue === false; + } + private getEligibleForAutoApprovalSpecialCase(toolData: IToolData): string | undefined { if (toolData.id === 'vscode_fetchWebPage_internal') { return 'fetch'; @@ -1040,12 +1049,22 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return true; } - private async shouldAutoConfirm(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined): Promise { + private async shouldAutoConfirm(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined, chatRequestId: string | undefined): Promise { const tool = this._tools.get(toolId); if (!tool) { return undefined; } + // Auto-Approve All permission level bypasses all tool confirmations, + // unless enterprise policy has explicitly disabled global auto-approve. + if (chatSessionResource) { + const model = this._chatService.getSession(chatSessionResource); + const request = model?.getRequests().at(-1); + if (isAutoApproveLevel(request?.modeInfo?.permissionLevel) && !this._isAutoApprovePolicyRestricted()) { + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; + } + } + if (!this.isToolEligibleForAutoApproval(tool.data)) { return undefined; } @@ -1077,7 +1096,17 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return undefined; } - private async shouldAutoConfirmPostExecution(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined): Promise { + private async shouldAutoConfirmPostExecution(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined, chatRequestId: string | undefined): Promise { + // Auto-Approve All permission level bypasses all post-execution confirmations, + // unless enterprise policy has explicitly disabled global auto-approve. + if (chatSessionResource) { + const model = this._chatService.getSession(chatSessionResource); + const request = model?.getRequests().at(-1); + if (isAutoApproveLevel(request?.modeInfo?.permissionLevel) && !this._isAutoApprovePolicyRestricted()) { + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; + } + } + if (this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove) && await this._checkGlobalAutoApprove()) { return { type: ToolConfirmKind.Setting, id: ChatConfiguration.GlobalAutoApprove }; } @@ -1186,7 +1215,6 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo // Clean up any pending tool calls that belong to this request for (const [toolCallId, invocation] of this._pendingToolCalls) { if (invocation.chatRequestId === requestId) { - invocation.cancelFromStreaming(ToolConfirmKind.Skipped); this._pendingToolCalls.delete(toolCallId); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts index c22a7d3391bd3..753e518fb2daf 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -29,10 +29,13 @@ import { Codicon } from '../../../../../../base/common/codicons.js'; import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidget.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; import { ScrollbarVisibility } from '../../../../../../base/common/scrollable.js'; import './media/chatQuestionCarousel.css'; +const PREVIOUS_QUESTION_ACTION_ID = 'workbench.action.chat.previousQuestion'; +const NEXT_QUESTION_ACTION_ID = 'workbench.action.chat.nextQuestion'; export interface IChatQuestionCarouselOptions { onSubmit: (answers: Map | undefined) => void; shouldAutoFocus?: boolean; @@ -46,15 +49,15 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent private _currentIndex = 0; private readonly _answers = new Map(); - private readonly _explicitlyAnsweredQuestionIds = new Set(); private _questionContainer: HTMLElement | undefined; private _closeButtonContainer: HTMLElement | undefined; - private _tabBar: HTMLElement | undefined; - private _tabItems: HTMLElement[] = []; - private readonly _questionTabIndicators = new Map(); - private _reviewIndex = -1; private _footerRow: HTMLElement | undefined; + private _stepIndicator: HTMLElement | undefined; + private _submitHint: HTMLElement | undefined; + private _submitButton: Button | undefined; + private _prevButton: Button | undefined; + private _nextButton: Button | undefined; private _skipAllButton: Button | undefined; private _isSkipped = false; @@ -82,6 +85,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent @IHoverService private readonly _hoverService: IHoverService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, ) { super(); @@ -133,10 +137,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._interactiveUIStore.value = interactiveStore; // Question container - const questionPanelId = `question-panel-${this.carousel.questions[0]?.id ?? 'default'}`; this._questionContainer = dom.$('.chat-question-carousel-content'); - this._questionContainer.setAttribute('role', 'tabpanel'); - this._questionContainer.id = questionPanelId; this.domNode.append(this._questionContainer); // Close/skip button (X) - placed in header row, only shown when allowSkip is true @@ -145,7 +146,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const skipAllTitle = localize('chat.questionCarousel.skipAllTitle', 'Skip all questions'); const skipAllButton = interactiveStore.add(new Button(this._closeButtonContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); skipAllButton.label = `$(${Codicon.close.id})`; - skipAllButton.element.classList.add('chat-question-nav-arrow', 'chat-question-close'); + skipAllButton.element.classList.add('chat-question-close'); skipAllButton.element.setAttribute('aria-label', skipAllTitle); interactiveStore.add(this._hoverService.setupDelayedHover(skipAllButton.element, { content: skipAllTitle })); this._skipAllButton = skipAllButton; @@ -153,70 +154,8 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const isSingleQuestion = this.carousel.questions.length === 1; - if (!isSingleQuestion) { - this._reviewIndex = this.carousel.questions.length; - - // Multi-question: Create tab bar with question tabs and Review tab - this._tabBar = dom.$('.chat-question-tab-bar'); - const tabList = dom.$('.chat-question-tabs'); - tabList.setAttribute('role', 'tablist'); - tabList.setAttribute('aria-label', localize('chat.questionCarousel.tabBarLabel', 'Questions')); - this._tabBar.appendChild(tabList); - - this.carousel.questions.forEach((question, index) => { - const tab = dom.$('.chat-question-tab'); - tab.setAttribute('role', 'tab'); - tab.setAttribute('aria-selected', index === 0 ? 'true' : 'false'); - tab.tabIndex = index === 0 ? 0 : -1; - tab.id = `question-tab-${question.id}-${index}`; - tab.setAttribute('aria-controls', questionPanelId); - - const displayTitle = this.getQuestionText(question.title); - const tabIndicator = dom.$('.chat-question-tab-indicator.codicon'); - const tabLabel = dom.$('span.chat-question-tab-label'); - tabLabel.textContent = displayTitle; - tab.append(tabIndicator, tabLabel); - tab.setAttribute('aria-label', displayTitle); - this._questionTabIndicators.set(question.id, tabIndicator); - - interactiveStore.add(dom.addDisposableListener(tab, dom.EventType.CLICK, () => { - this.saveCurrentAnswer(); - this._currentIndex = index; - this.renderCurrentQuestion(true); - tab.focus(); - })); - - tabList.appendChild(tab); - this._tabItems.push(tab); - }); - - // Review tab - const reviewTab = dom.$('.chat-question-tab.no-icon'); - reviewTab.setAttribute('role', 'tab'); - reviewTab.setAttribute('aria-selected', 'false'); - reviewTab.tabIndex = -1; - reviewTab.id = 'question-tab-review'; - reviewTab.setAttribute('aria-controls', questionPanelId); - const reviewLabel = localize('chat.questionCarousel.review', 'Review'); - reviewTab.textContent = reviewLabel; - reviewTab.setAttribute('aria-label', reviewLabel); - interactiveStore.add(dom.addDisposableListener(reviewTab, dom.EventType.CLICK, () => { - this.saveCurrentAnswer(); - this._currentIndex = this._reviewIndex; - this.renderCurrentQuestion(true); - reviewTab.focus(); - })); - tabList.appendChild(reviewTab); - this._tabItems.push(reviewTab); - - // Controls container for close button only - if (this._closeButtonContainer) { - const controlsContainer = dom.$('.chat-question-tab-controls'); - controlsContainer.appendChild(this._closeButtonContainer); - this._tabBar.appendChild(controlsContainer); - } - - this.domNode.insertBefore(this._tabBar, this._questionContainer!); + if (!isSingleQuestion && this._closeButtonContainer) { + this.domNode.insertBefore(this._closeButtonContainer, this._questionContainer!); } // Register event listeners @@ -224,47 +163,19 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent interactiveStore.add(this._skipAllButton.onDidClick(() => this.ignore())); } - // Register keyboard navigation - handle Enter on text inputs and freeform textareas + // Register keyboard navigation interactiveStore.add(dom.addDisposableListener(this.domNode, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); if (event.keyCode === KeyCode.Escape && this.carousel.allowSkip) { e.preventDefault(); e.stopPropagation(); this.ignore(); - } else if (!isSingleQuestion && (event.keyCode === KeyCode.RightArrow || event.keyCode === KeyCode.LeftArrow)) { - // Arrow L/R navigates tabs from anywhere in the carousel, - // except when focus is in a text input or textarea (where arrows move cursor) - const target = e.target as HTMLElement; - const isTextInput = target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'text'; - const isTextarea = target.tagName === 'TEXTAREA'; - if (!isTextInput && !isTextarea) { - e.preventDefault(); - e.stopPropagation(); - const totalTabs = this._tabItems.length; // includes Review tab - if (event.keyCode === KeyCode.RightArrow) { - if (this._currentIndex < totalTabs - 1) { - this.saveCurrentAnswer(); - this._currentIndex++; - this.renderCurrentQuestion(true); - this._tabItems[this._currentIndex]?.focus(); - } - } else { - if (this._currentIndex > 0) { - this.saveCurrentAnswer(); - this._currentIndex--; - this.renderCurrentQuestion(true); - this._tabItems[this._currentIndex]?.focus(); - } - } - } } else if (event.keyCode === KeyCode.Enter && (event.metaKey || event.ctrlKey)) { // Cmd/Ctrl+Enter submits immediately from anywhere e.preventDefault(); e.stopPropagation(); this.submit(); } else if (event.keyCode === KeyCode.Enter && !event.shiftKey) { - // Handle Enter key for text inputs and freeform textareas, not radio/checkbox or buttons - // Buttons have their own Enter/Space handling via Button class const target = e.target as HTMLElement; const isTextInput = target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'text'; const isFreeformTextarea = target.tagName === 'TEXTAREA' && target.classList.contains('chat-question-freeform-textarea'); @@ -287,9 +198,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent */ private saveCurrentAnswer(): void { const currentQuestion = this.carousel.questions[this._currentIndex]; - if (!currentQuestion) { - return; // Review tab or out of bounds - } const answer = this.getCurrentAnswer(); if (answer !== undefined) { this._answers.set(currentQuestion.id, answer); @@ -320,6 +228,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._currentIndex = newIndex; this.persistDraftState(); this.renderCurrentQuestion(true); + this.domNode.focus(); } } @@ -329,24 +238,14 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent */ private handleNextOrSubmit(): void { this.saveCurrentAnswer(); - const currentQuestion = this.carousel.questions[this._currentIndex]; - if (currentQuestion && this.getCurrentAnswer() !== undefined) { - this._explicitlyAnsweredQuestionIds.add(currentQuestion.id); - this.updateQuestionTabIndicators(); - } if (this._currentIndex < this.carousel.questions.length - 1) { // Move to next question this._currentIndex++; this.persistDraftState(); this.renderCurrentQuestion(true); - } else if (this.carousel.questions.length > 1) { - // Multi-question: navigate to Review tab - this._currentIndex = this._reviewIndex; - this.renderCurrentQuestion(true); - this._tabItems[this._currentIndex]?.focus(); } else { - // Single question: submit directly + // Submit this._options.onSubmit(this._answers); this.hideAndShowSummary(); } @@ -357,10 +256,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent */ private submit(): void { this.saveCurrentAnswer(); - const currentQuestion = this.carousel.questions[this._currentIndex]; - if (currentQuestion) { - this._explicitlyAnsweredQuestionIds.add(currentQuestion.id); - } this._options.onSubmit(this._answers); this.hideAndShowSummary(); } @@ -413,16 +308,16 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._freeformTextareas.clear(); // Clear references to disposed elements + this._prevButton = undefined; + this._nextButton = undefined; + this._submitButton = undefined; this._skipAllButton = undefined; this._questionContainer = undefined; this._closeButtonContainer = undefined; - this._tabBar = undefined; - this._tabItems = []; - this._questionTabIndicators.clear(); - this._reviewIndex = -1; this._footerRow = undefined; + this._stepIndicator = undefined; + this._submitHint = undefined; this._inputScrollable = undefined; - this._explicitlyAnsweredQuestionIds.clear(); } private layoutInputScrollable(inputScrollable: DomScrollableElement): void { @@ -451,12 +346,15 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const availableScrollableHeight = Math.floor(maxContainerHeight - contentVerticalPadding - nonScrollableContentHeight); const constrainedScrollableHeight = Math.max(0, availableScrollableHeight); + const constrainedScrollableHeightPx = `${constrainedScrollableHeight}px`; // Constrain the content element (DomScrollableElement._element) so that // scanDomNode sees clientHeight < scrollHeight and enables scrolling. // The wrapper inherits the same constraint via CSS flex. - scrollableContent.style.height = `${constrainedScrollableHeight}px`; - scrollableContent.style.maxHeight = `${constrainedScrollableHeight}px`; + if (scrollableContent.style.height !== constrainedScrollableHeightPx || scrollableContent.style.maxHeight !== constrainedScrollableHeightPx) { + scrollableContent.style.height = constrainedScrollableHeightPx; + scrollableContent.style.maxHeight = constrainedScrollableHeightPx; + } inputScrollable.scanDomNode(); } @@ -636,113 +534,40 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._multiSelectCheckboxes.clear(); this._freeformTextareas.clear(); - // Remove footer if it exists from a previous Review render - if (this._footerRow) { - this._footerRow.remove(); - this._footerRow = undefined; - } - // Clear previous content dom.clearNode(this._questionContainer); - const isSingleQuestion = this.carousel.questions.length === 1; - const isReview = !isSingleQuestion && this._currentIndex === this._reviewIndex; - - // Update tab bar active state for multi-question carousels - if (!isSingleQuestion) { - this._tabItems.forEach((tab, index) => { - const isActive = index === this._currentIndex; - tab.classList.toggle('active', isActive); - tab.setAttribute('aria-selected', String(isActive)); - tab.tabIndex = isActive ? 0 : -1; - }); - // Link the panel to the active tab for screen readers - const activeTab = this._tabItems[this._currentIndex]; - if (activeTab) { - this._questionContainer.setAttribute('aria-labelledby', activeTab.id); - } - this.updateQuestionTabIndicators(); - } - - if (isReview) { - this.renderReviewPanel(questionRenderStore); - } else { - this.renderQuestionPanel(questionRenderStore, isSingleQuestion); - } - - // Update aria-label to reflect the current question - this._updateAriaLabel(); - - // In screen reader mode, focus the container and announce the question - if (focusContainerForScreenReader && this._accessibilityService.isScreenReaderOptimized()) { - this._focusContainerAndAnnounce(); - } - - this._onDidChangeHeight.fire(); - } - - /** - * Renders a question panel (title, message, input) inside the question container. - */ - private renderQuestionPanel(questionRenderStore: DisposableStore, isSingleQuestion: boolean): void { const question = this.carousel.questions[this._currentIndex]; - if (!question || !this._questionContainer) { + if (!question) { return; } - // Render question header row with title and close button (single question only) - if (isSingleQuestion) { - const headerRow = dom.$('.chat-question-header-row'); - const titleRow = dom.$('.chat-question-title-row'); - - if (question.title) { - const title = dom.$('.chat-question-title'); - const questionText = question.title; - const messageContent = this.getQuestionText(questionText); - - title.setAttribute('aria-label', messageContent); - - if (question.message !== undefined) { - const messageMd = isMarkdownString(questionText) ? MarkdownString.lift(questionText) : new MarkdownString(questionText); - const renderedTitle = questionRenderStore.add(this._markdownRendererService.render(messageMd)); - title.appendChild(renderedTitle.element); - } else { - const parenMatch = messageContent.match(/^(.+?)\s*(\([^)]+\))\s*$/); - if (parenMatch) { - const mainTitle = dom.$('span.chat-question-title-main'); - mainTitle.textContent = parenMatch[1]; - title.appendChild(mainTitle); - - const subtitle = dom.$('span.chat-question-title-subtitle'); - subtitle.textContent = ' ' + parenMatch[2]; - title.appendChild(subtitle); - } else { - title.textContent = messageContent; - } - } - titleRow.appendChild(title); - } + // Render unified question title (message ?? title) + const headerRow = dom.$('.chat-question-header-row'); + const titleRow = dom.$('.chat-question-title-row'); - if (this._closeButtonContainer) { - titleRow.appendChild(this._closeButtonContainer); - } + const questionText = question.message ?? question.title; + if (questionText) { + const title = dom.$('.chat-question-title'); + const messageContent = this.getQuestionText(questionText); + title.setAttribute('aria-label', messageContent); - headerRow.appendChild(titleRow); - this._questionContainer.appendChild(headerRow); + const messageMd = isMarkdownString(questionText) ? MarkdownString.lift(questionText) : new MarkdownString(questionText); + const renderedTitle = questionRenderStore.add(this._markdownRendererService.render(messageMd)); + title.appendChild(renderedTitle.element); + titleRow.appendChild(title); } - // Render full question text below the header row - if (question.message) { - const messageEl = dom.$('.chat-question-message'); - if (isMarkdownString(question.message)) { - const renderedMessage = questionRenderStore.add(this._markdownRendererService.render(MarkdownString.lift(question.message))); - messageEl.appendChild(renderedMessage.element); - } else { - messageEl.textContent = this.getQuestionText(question.message); - } - this._questionContainer.appendChild(messageEl); + headerRow.appendChild(titleRow); + + // For single-question carousels, add close button inside the title row + const isSingleQuestion = this.carousel.questions.length === 1; + if (isSingleQuestion && this._closeButtonContainer) { + titleRow.appendChild(this._closeButtonContainer); } + this._questionContainer.appendChild(headerRow); + // Render input based on question type const inputContainer = dom.$('.chat-question-input-container'); this.renderInput(inputContainer, question); @@ -757,10 +582,24 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent inputScrollableNode.classList.add('chat-question-input-scrollable'); this._questionContainer.appendChild(inputScrollableNode); - const inputResizeObserver = questionRenderStore.add(new dom.DisposableResizeObserver(() => this.layoutInputScrollable(inputScrollable))); + let relayoutScheduled = false; + const relayoutScheduler = questionRenderStore.add(new MutableDisposable()); + const scheduleLayoutInputScrollable = () => { + if (relayoutScheduled) { + return; + } + + relayoutScheduled = true; + relayoutScheduler.value = dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.domNode), () => { + relayoutScheduled = false; + this.layoutInputScrollable(inputScrollable); + }); + }; + + const inputResizeObserver = questionRenderStore.add(new dom.DisposableResizeObserver(() => scheduleLayoutInputScrollable())); questionRenderStore.add(inputResizeObserver.observe(inputScrollableNode)); questionRenderStore.add(inputResizeObserver.observe(inputContainer)); - questionRenderStore.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.domNode), () => this.layoutInputScrollable(inputScrollable))); + scheduleLayoutInputScrollable(); this.layoutInputScrollable(inputScrollable); questionRenderStore.add(dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.domNode), () => { inputContainer.scrollTop = 0; @@ -768,77 +607,165 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent inputScrollable.setScrollPosition({ scrollTop: 0, scrollLeft: 0 }); inputScrollable.scanDomNode(); })); + + // Render footer for multi-question carousels or single-question carousels. + if (!isSingleQuestion) { + this.renderFooter(); + } else { + this.renderSingleQuestionFooter(); + } + + // Update aria-label to reflect the current question + this._updateAriaLabel(); + + // In screen reader mode, focus the container and announce the question + // This must happen after all render calls to avoid focus being stolen + if (focusContainerForScreenReader && this._accessibilityService.isScreenReaderOptimized()) { + this._focusContainerAndAnnounce(); + } + + this._onDidChangeHeight.fire(); } /** - * Renders the review panel with a summary of all answers and a submit footer. + * Renders or updates the persistent footer with nav arrows, step indicator, and submit button. */ - private renderReviewPanel(questionRenderStore: DisposableStore): void { - if (!this._questionContainer) { - return; - } + private renderFooter(): void { + if (!this._footerRow) { + const interactiveStore = this._interactiveUIStore.value; + if (!interactiveStore) { + return; + } - // Render inline review summary. - // If no explicit answers exist yet, show a single empty-state label. - // If some explicit answers exist, show all questions and mark missing ones as not answered yet. - const summaryContainer = dom.$('.chat-question-carousel-summary'); - const answeredCount = this.carousel.questions.filter(q => this._explicitlyAnsweredQuestionIds.has(q.id)).length; + this._footerRow = dom.$('.chat-question-footer-row'); - if (answeredCount === 0) { - const emptyLabel = dom.$('div.chat-question-summary-empty'); - emptyLabel.textContent = localize('chat.questionCarousel.noQuestionsAnsweredYet', 'No questions answered yet'); - summaryContainer.appendChild(emptyLabel); - this._questionContainer.appendChild(summaryContainer); - } else { - for (const question of this.carousel.questions) { - const summaryItem = dom.$('.chat-question-summary-item'); - - const questionRow = dom.$('div.chat-question-summary-label'); - const questionText = question.message ?? question.title; - let labelText = typeof questionText === 'string' ? questionText : questionText.value; - labelText = labelText.replace(/[:\s]+$/, ''); - questionRow.textContent = localize('chat.questionCarousel.summaryQuestion', 'Q: {0}', labelText); - summaryItem.appendChild(questionRow); - - const hasExplicitAnswer = this._explicitlyAnsweredQuestionIds.has(question.id); - const answer = this._answers.get(question.id); - - if (hasExplicitAnswer && answer !== undefined) { - const formattedAnswer = this.formatAnswerForSummary(question, answer); - const answerRow = dom.$('div.chat-question-summary-answer'); - answerRow.textContent = localize('chat.questionCarousel.summaryAnswer', 'A: {0}', formattedAnswer); - summaryItem.appendChild(answerRow); - } else { - const unanswered = dom.$('div.chat-question-summary-unanswered'); - unanswered.textContent = localize('chat.questionCarousel.notAnsweredYet', 'Not answered yet'); - summaryItem.appendChild(unanswered); - } + // Left side: nav arrows + step indicator + const leftControls = dom.$('.chat-question-footer-left.chat-question-carousel-nav'); + leftControls.setAttribute('role', 'navigation'); + leftControls.setAttribute('aria-label', localize('chat.questionCarousel.navigation', 'Question navigation')); - summaryContainer.appendChild(summaryItem); - } + const arrowsContainer = dom.$('.chat-question-nav-arrows'); + + const previousLabel = this.getLabelWithKeybinding(localize('previous', 'Previous'), PREVIOUS_QUESTION_ACTION_ID); + const prevButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); + prevButton.element.classList.add('chat-question-nav-arrow', 'chat-question-nav-prev'); + prevButton.label = `$(${Codicon.chevronLeft.id})`; + prevButton.element.setAttribute('aria-label', previousLabel); + interactiveStore.add(this._hoverService.setupDelayedHover(prevButton.element, { content: previousLabel })); + interactiveStore.add(prevButton.onDidClick(() => this.navigate(-1))); + this._prevButton = prevButton; + + const nextLabel = this.getLabelWithKeybinding(localize('next', 'Next'), NEXT_QUESTION_ACTION_ID); + const nextButton = interactiveStore.add(new Button(arrowsContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true })); + nextButton.element.classList.add('chat-question-nav-arrow', 'chat-question-nav-next'); + nextButton.label = `$(${Codicon.chevronRight.id})`; + nextButton.element.setAttribute('aria-label', nextLabel); + interactiveStore.add(this._hoverService.setupDelayedHover(nextButton.element, { content: nextLabel })); + interactiveStore.add(nextButton.onDidClick(() => this.navigate(1))); + this._nextButton = nextButton; + + leftControls.appendChild(arrowsContainer); + + this._stepIndicator = dom.$('.chat-question-step-indicator'); + leftControls.appendChild(this._stepIndicator); + + this._footerRow.appendChild(leftControls); - this._questionContainer.appendChild(summaryContainer); + // Right side: hint + submit + const rightControls = dom.$('.chat-question-footer-right'); + + const hint = dom.$('span.chat-question-submit-hint'); + hint.textContent = isMacintosh + ? localize('chat.questionCarousel.submitHintMac', '\u2318\u23CE to submit') + : localize('chat.questionCarousel.submitHintOther', 'Ctrl+Enter to submit'); + rightControls.appendChild(hint); + this._submitHint = hint; + + const submitButton = interactiveStore.add(new Button(rightControls, { ...defaultButtonStyles })); + submitButton.element.classList.add('chat-question-submit-button'); + submitButton.label = localize('submit', 'Submit'); + interactiveStore.add(submitButton.onDidClick(() => this.submit())); + this._submitButton = submitButton; + + this._footerRow.appendChild(rightControls); + this.domNode.append(this._footerRow); } - // Footer with Submit/Cancel appears only once at least one question is answered. - if (answeredCount > 0) { + this.updateFooterState(); + } + + /** + * Updates the footer nav button enabled state and step indicator text. + */ + private updateFooterState(): void { + if (this._prevButton) { + this._prevButton.enabled = this._currentIndex > 0; + } + if (this._nextButton) { + this._nextButton.enabled = this._currentIndex < this.carousel.questions.length - 1; + } + if (this._stepIndicator) { + this._stepIndicator.textContent = localize( + 'chat.questionCarousel.stepIndicator', + '{0}/{1}', + this._currentIndex + 1, + this.carousel.questions.length + ); + } + if (this._submitButton) { + const isLastQuestion = this._currentIndex === this.carousel.questions.length - 1; + this._submitButton.element.style.display = isLastQuestion ? '' : 'none'; + if (this._submitHint) { + this._submitHint.style.display = isLastQuestion ? '' : 'none'; + } + } + } + + /** + * Renders a simplified footer with just a submit button for single-question multi-select carousels. + */ + private renderSingleQuestionFooter(): void { + if (!this._footerRow) { + const interactiveStore = this._interactiveUIStore.value; + if (!interactiveStore) { + return; + } + this._footerRow = dom.$('.chat-question-footer-row'); + // Spacer to push controls to the right + const leftControls = dom.$('.chat-question-footer-left.chat-question-carousel-nav'); + leftControls.setAttribute('role', 'navigation'); + leftControls.setAttribute('aria-label', localize('chat.questionCarousel.navigation', 'Question navigation')); + this._footerRow.appendChild(leftControls); + + const rightControls = dom.$('.chat-question-footer-right'); + const hint = dom.$('span.chat-question-submit-hint'); hint.textContent = isMacintosh ? localize('chat.questionCarousel.submitHintMac', '\u2318\u23CE to submit') : localize('chat.questionCarousel.submitHintOther', 'Ctrl+Enter to submit'); - this._footerRow.appendChild(hint); + rightControls.appendChild(hint); + this._submitHint = hint; - const submitButton = questionRenderStore.add(new Button(this._footerRow, { ...defaultButtonStyles })); + const submitButton = interactiveStore.add(new Button(rightControls, { ...defaultButtonStyles })); submitButton.element.classList.add('chat-question-submit-button'); submitButton.label = localize('submit', 'Submit'); - questionRenderStore.add(submitButton.onDidClick(() => this.submit())); + interactiveStore.add(submitButton.onDidClick(() => this.submit())); + this._submitButton = submitButton; + this._footerRow.appendChild(rightControls); this.domNode.append(this._footerRow); } } + private getLabelWithKeybinding(label: string, actionId: string): string { + const keybindingLabel = this._keybindingService.lookupKeybinding(actionId, this._contextKeyService)?.getLabel(); + return keybindingLabel + ? localize('chat.questionCarousel.labelWithKeybinding', '{0} ({1})', label, keybindingLabel) + : label; + } + private renderInput(container: HTMLElement, question: IChatQuestion): void { switch (question.type) { case 'text': @@ -925,7 +852,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const listItems: HTMLElement[] = []; const indicators: HTMLElement[] = []; - const updateSelection = (newIndex: number, isUserInitiated: boolean = false) => { + const updateSelection = (newIndex: number) => { // Update visual state listItems.forEach((item, i) => { const isSelected = i === newIndex; @@ -944,9 +871,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent if (data) { data.selectedIndex = newIndex; } - if (isUserInitiated) { - this.updateQuestionTabIndicators(); - } this.saveCurrentAnswer(); }; @@ -975,11 +899,12 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const label = dom.$('.chat-question-list-label'); const separatorIndex = option.label.indexOf(' - '); if (separatorIndex !== -1) { - const titleSpan = dom.$('div.chat-question-list-label-title'); + listItem.classList.add('has-description'); + const titleSpan = dom.$('span.chat-question-list-label-title'); titleSpan.textContent = option.label.substring(0, separatorIndex); label.appendChild(titleSpan); - const descSpan = dom.$('div.chat-question-list-label-desc'); + const descSpan = dom.$('span.chat-question-list-label-desc'); descSpan.textContent = option.label.substring(separatorIndex + 3); label.appendChild(descSpan); } else { @@ -996,7 +921,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._inputBoxes.add(dom.addDisposableListener(listItem, dom.EventType.CLICK, (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); - updateSelection(index, true); + updateSelection(index); const freeform = this._freeformTextareas.get(question.id); if (freeform) { freeform.value = ''; @@ -1042,17 +967,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent // clear when we start typing in freeform this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => { if (freeformTextarea.value.length > 0) { - updateSelection(-1, true); - } - })); - - this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { - const event = new StandardKeyboardEvent(e); - if (event.keyCode === KeyCode.UpArrow && freeformTextarea.selectionStart === 0 && freeformTextarea.selectionEnd === 0 && listItems.length) { - e.preventDefault(); - const lastIndex = listItems.length - 1; - updateSelection(lastIndex, true); - listItems[lastIndex].focus(); + updateSelection(-1); + } else { + this.saveCurrentAnswer(); } })); @@ -1071,16 +988,11 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent if (event.keyCode === KeyCode.DownArrow) { e.preventDefault(); - if (data.selectedIndex >= listItems.length - 1) { - updateSelection(-1); - freeformTextarea.focus(); - return; - } newIndex = Math.min(data.selectedIndex + 1, listItems.length - 1); } else if (event.keyCode === KeyCode.UpArrow) { e.preventDefault(); newIndex = Math.max(data.selectedIndex - 1, 0); - } else if (event.keyCode === KeyCode.Enter || event.keyCode === KeyCode.Space) { + } else if ((event.keyCode === KeyCode.Enter || event.keyCode === KeyCode.Space) && !event.metaKey && !event.ctrlKey) { // Enter confirms current selection and advances to next question e.preventDefault(); e.stopPropagation(); @@ -1091,17 +1003,17 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const numberIndex = event.keyCode - KeyCode.Digit1; if (numberIndex < listItems.length) { e.preventDefault(); - updateSelection(numberIndex, true); + updateSelection(numberIndex); } else if (numberIndex === listItems.length) { e.preventDefault(); - updateSelection(-1, true); + updateSelection(-1); freeformTextarea.focus(); } return; } if (newIndex !== data.selectedIndex && newIndex >= 0) { - updateSelection(newIndex, true); + updateSelection(newIndex); } })); @@ -1188,11 +1100,12 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const label = dom.$('.chat-question-list-label'); const separatorIndex = option.label.indexOf(' - '); if (separatorIndex !== -1) { - const titleSpan = dom.$('div.chat-question-list-label-title'); + listItem.classList.add('has-description'); + const titleSpan = dom.$('span.chat-question-list-label-title'); titleSpan.textContent = option.label.substring(0, separatorIndex); label.appendChild(titleSpan); - const descSpan = dom.$('div.chat-question-list-label-desc'); + const descSpan = dom.$('span.chat-question-list-label-desc'); descSpan.textContent = option.label.substring(separatorIndex + 3); label.appendChild(descSpan); } else { @@ -1211,7 +1124,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent this._inputBoxes.add(checkbox.onChange(() => { listItem.classList.toggle('checked', checkbox.checked); listItem.setAttribute('aria-selected', String(checkbox.checked)); - this.updateQuestionTabIndicators(); this.saveCurrentAnswer(); })); @@ -1259,18 +1171,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const autoResize = this.setupTextareaAutoResize(freeformTextarea); this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => this.saveCurrentAnswer())); - this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { - const event = new StandardKeyboardEvent(e); - if (event.keyCode === KeyCode.UpArrow && freeformTextarea.selectionStart === 0 && freeformTextarea.selectionEnd === 0 && listItems.length) { - e.preventDefault(); - focusedIndex = listItems.length - 1; - listItems[focusedIndex].focus(); - } - })); - this._inputBoxes.add(dom.addDisposableListener(freeformTextarea, dom.EventType.INPUT, () => { - this.updateQuestionTabIndicators(); - })); - freeformContainer.appendChild(freeformTextarea); container.appendChild(freeformContainer); this._freeformTextareas.set(question.id, freeformTextarea); @@ -1286,17 +1186,13 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent if (event.keyCode === KeyCode.DownArrow) { e.preventDefault(); - if (focusedIndex >= listItems.length - 1) { - freeformTextarea.focus(); - return; - } focusedIndex = Math.min(focusedIndex + 1, listItems.length - 1); listItems[focusedIndex].focus(); } else if (event.keyCode === KeyCode.UpArrow) { e.preventDefault(); focusedIndex = Math.max(focusedIndex - 1, 0); listItems[focusedIndex].focus(); - } else if (event.keyCode === KeyCode.Enter) { + } else if (event.keyCode === KeyCode.Enter && !event.metaKey && !event.ctrlKey) { e.preventDefault(); e.stopPropagation(); this.handleNextOrSubmit(); @@ -1358,20 +1254,19 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent if (data && data.selectedIndex >= 0) { selectedValue = question.options?.[data.selectedIndex]?.value; } + // Find default option if nothing selected (defaultValue is the option id) + if (selectedValue === undefined && typeof question.defaultValue === 'string') { + const defaultOption = question.options?.find(opt => opt.id === question.defaultValue); + selectedValue = defaultOption?.value; + } - // For single-select: freeform takes priority over selection. + // For single-select: if freeform is provided, use ONLY freeform (ignore selection) const freeformTextarea = this._freeformTextareas.get(question.id); const freeformValue = freeformTextarea?.value !== '' ? freeformTextarea?.value : undefined; if (freeformValue) { + // Freeform takes priority - ignore selectedValue return { selectedValue: undefined, freeformValue }; } - - // Find default option if nothing selected and no freeform text (defaultValue is the option id) - if (selectedValue === undefined && typeof question.defaultValue === 'string') { - const defaultOption = question.options?.find(opt => opt.id === question.defaultValue); - selectedValue = defaultOption?.value; - } - if (selectedValue !== undefined) { return { selectedValue, freeformValue: undefined }; } @@ -1436,13 +1331,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent for (const question of this.carousel.questions) { const answer = this._answers.get(question.id); - if (answer === undefined) { - continue; - } const summaryItem = dom.$('.chat-question-summary-item'); - // Question row with Q: prefix const questionRow = dom.$('div.chat-question-summary-label'); const questionText = question.message ?? question.title; let labelText = typeof questionText === 'string' ? questionText : questionText.value; @@ -1450,11 +1341,16 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent questionRow.textContent = localize('chat.questionCarousel.summaryQuestion', 'Q: {0}', labelText); summaryItem.appendChild(questionRow); - // Answer row with A: prefix - const formattedAnswer = this.formatAnswerForSummary(question, answer); - const answerRow = dom.$('div.chat-question-summary-answer'); - answerRow.textContent = localize('chat.questionCarousel.summaryAnswer', 'A: {0}', formattedAnswer); - summaryItem.appendChild(answerRow); + if (answer !== undefined) { + const formattedAnswer = this.formatAnswerForSummary(question, answer); + const answerRow = dom.$('div.chat-question-summary-answer-title'); + answerRow.textContent = localize('chat.questionCarousel.summaryAnswer', 'A: {0}', formattedAnswer); + summaryItem.appendChild(answerRow); + } else { + const unanswered = dom.$('div.chat-question-summary-unanswered'); + unanswered.textContent = localize('chat.questionCarousel.notAnsweredYet', 'Not answered yet'); + summaryItem.appendChild(unanswered); + } summaryContainer.appendChild(summaryItem); } @@ -1480,6 +1376,10 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent } return selectedLabel ?? String(selectedValue ?? ''); } + // Handle case where selectedValue was stripped during JSON serialization (undefined values are omitted by JSON.stringify) + if (typeof answer === 'object' && answer !== null && hasKey(answer, { freeformValue: true })) { + return (answer as { freeformValue?: string }).freeformValue ?? ''; + } const label = question.options?.find(opt => opt.value === answer)?.label; return label ?? String(answer); } @@ -1513,21 +1413,6 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent return renderAsPlaintext(md); } - - - private updateQuestionTabIndicators(): void { - for (const question of this.carousel.questions) { - const indicator = this._questionTabIndicators.get(question.id); - if (!indicator) { - continue; - } - - const hasExplicitAnswer = this._explicitlyAnsweredQuestionIds.has(question.id); - indicator.classList.toggle('codicon-check', hasExplicitAnswer); - indicator.classList.toggle('codicon-circle-filled', !hasExplicitAnswer); - } - } - hasSameContent(other: IChatRendererContent, _followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { // does not have same content when it is not skipped and is active and we stop the response if (!this._isSkipped && !this.carousel.isUsed && isResponseVM(element) && element.isComplete) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index bbab1de37aa36..d695b515e884a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -500,6 +500,10 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen height: viewportHeight, scrollHeight: contentHeight }); + + // Re-evaluate hover feedback as content grows past the max height, + // reusing the already-measured contentHeight to avoid an extra layout read. + this.updateDropdownClickability(contentHeight); } private scrollToBottom(): void { @@ -651,8 +655,17 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen return !(!strippedContent || strippedContent === titleToCompare); } - private updateDropdownClickability(): void { - const allowExpansion = this.shouldAllowExpansion(); + private updateDropdownClickability(knownContentHeight?: number): void { + let allowExpansion = this.shouldAllowExpansion(); + + // don't allow feedback on fixed scrolling before reaching max height. + if (allowExpansion && this.fixedScrollingMode && !this.streamingCompleted && !this.element.isComplete && this.wrapper) { + const contentHeight = knownContentHeight ?? this.wrapper.scrollHeight; + if (contentHeight <= THINKING_SCROLL_MAX_HEIGHT) { + allowExpansion = false; + } + } + if (!allowExpansion && this.isExpanded()) { this.setExpanded(false); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css index ae28fb9038ebc..59814a560f8f6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatQuestionCarousel.css @@ -14,14 +14,10 @@ .interactive-session .interactive-input-part .interactive-input-and-edit-session > .chat-question-carousel-widget-container .chat-question-carousel-container { margin: 0; border: 1px solid var(--vscode-input-border, transparent); - background-color: var(--vscode-chat-list-background); + background-color: var(--vscode-panel-background); border-radius: var(--vscode-cornerRadius-large); } -.interactive-session .interactive-input-part.compact > .chat-question-carousel-widget-container .chat-question-carousel-container { - border-radius: var(--vscode-cornerRadius-small); -} - /* general questions styling */ .interactive-session .chat-question-carousel-container { margin: 8px 0; @@ -32,6 +28,7 @@ overflow: hidden; container-type: inline-size; max-height: min(420px, 45vh); + position: relative; } /* input part wrapper */ @@ -51,7 +48,6 @@ .interactive-session .interactive-input-part > .chat-question-carousel-widget-container .chat-question-carousel-content, .interactive-session .interactive-input-part .interactive-input-and-edit-session > .chat-question-carousel-widget-container .chat-question-carousel-content { - flex: 1; min-height: 0; } @@ -59,16 +55,13 @@ .interactive-session .chat-question-carousel-container .chat-question-carousel-content { display: flex; flex-direction: column; - flex: 1; min-height: 0; - background: var(--vscode-chat-list-background); overflow: hidden; .chat-question-header-row { display: flex; flex-direction: column; flex-shrink: 0; - background: var(--vscode-chat-list-background); overflow: hidden; .chat-question-title-row { @@ -77,7 +70,7 @@ align-items: center; gap: 8px; min-width: 0; - padding: 4px 8px 4px 16px; + padding: 8px 8px 8px 16px; border-bottom: 1px solid var(--vscode-chat-requestBorder); } @@ -104,15 +97,6 @@ margin: 0; } } - - .chat-question-title-main { - font-weight: 500; - } - - .chat-question-title-subtitle { - font-weight: normal; - color: var(--vscode-descriptionForeground); - } } .chat-question-close-container { @@ -134,38 +118,18 @@ } } } - .chat-question-message { - flex-shrink: 0; - font-size: var(--vscode-chat-font-size-body-s); - line-height: 1.4; - word-wrap: break-word; - overflow-wrap: break-word; - padding: 16px; - - .rendered-markdown { - a { - color: var(--vscode-textLink-foreground); - } - - a:hover, - a:active { - color: var(--vscode-textLink-activeForeground); - } +} - p { - margin: 0; - } - } - } +/* Extra right padding when close button is absolutely positioned (multi-question) */ +.interactive-session .chat-question-carousel-container:has(> .chat-question-close-container) .chat-question-title-row { + padding-right: 36px; } /* questions list and freeform area */ .interactive-session .chat-question-carousel-container .chat-question-input-container { display: flex; flex-direction: column; - margin-top: 4px; - padding-right: 14px; - padding-bottom: 12px; + padding: 8px; min-width: 0; &::after { @@ -177,7 +141,6 @@ /* some hackiness to get the focus looking right */ .chat-question-list-item:focus, - .chat-question-list-item:focus-visible, .chat-question-list:focus { outline: none; } @@ -186,14 +149,13 @@ display: flex; flex-direction: column; outline: none; - margin: 0 8px; - padding: 0 0 4px 0; + padding: 0; .chat-question-list-item { display: flex; - align-items: flex-start; + align-items: center; gap: 12px; - padding: 8px 8px 8px 12px; + padding: 6px 8px; cursor: pointer; border-radius: var(--vscode-cornerRadius-medium); user-select: none; @@ -206,6 +168,7 @@ justify-content: center; flex-shrink: 0; margin-left: auto; + align-self: flex-start; margin-top: 2px; } @@ -234,13 +197,28 @@ } } + .chat-question-list-item.has-description { + align-items: flex-start; + + .chat-question-list-number { + line-height: 1.4; + font-size: var(--vscode-chat-font-size-body-s); + font-weight: 500; + } + + .chat-question-list-checkbox { + /* Title line-height is ~17px (1.4 * body-s), checkbox is 16px: 1px offset */ + margin-top: 1px; + } + } + .chat-question-list-item:hover { background-color: var(--vscode-list-hoverBackground); } /* Single-select: highlight entire row when selected */ .chat-question-list-item.selected { - background-color: var(--vscode-list-activeSelectionBackground); + background-color: var(--vscode-list-hoverBackground); color: var(--vscode-list-activeSelectionForeground); .chat-question-label { @@ -262,7 +240,7 @@ } .chat-question-list-item.selected:hover { - background-color: var(--vscode-list-activeSelectionBackground); + background-color: var(--vscode-list-hoverBackground); } /* Checkbox for multi-select */ @@ -276,10 +254,11 @@ } .chat-question-freeform { + margin: 0; display: flex; flex-direction: row; align-items: center; - margin: 0px 8px 0 20px; + padding: 4px 8px; gap: 12px; .chat-question-freeform-number { @@ -296,11 +275,11 @@ width: 100%; min-height: 24px; max-height: 200px; - padding: 0; - border: none; + padding: 3px 8px; + border: 1px solid var(--vscode-input-border, var(--vscode-chat-requestBorder)); background-color: var(--vscode-input-background); color: var(--vscode-input-foreground); - border-radius: var(--vscode-cornerRadius-medium); + border-radius: 4px; resize: none; font-family: var(--vscode-chat-font-family, inherit); font-size: var(--vscode-chat-font-size-body-s); @@ -310,26 +289,24 @@ } .chat-question-freeform-textarea:focus { - outline: none; + outline: 1px solid var(--vscode-focusBorder); + border-color: var(--vscode-focusBorder); } .chat-question-freeform-textarea::placeholder { color: var(--vscode-input-placeholderForeground); } - &:focus-within .chat-question-freeform-number { - color: var(--vscode-list-activeSelectionForeground); - } - } /* todo: change to use keybinding service so we don't have to recreate this */ .chat-question-list-number, .chat-question-freeform-number { - font-size: 11px; + font-size: var(--vscode-chat-font-size-body-s); color: var(--vscode-descriptionForeground); flex-shrink: 0; - line-height: 1rem; + min-width: 1ch; + text-align: right; } } @@ -338,120 +315,84 @@ } .interactive-session .chat-question-carousel-container .chat-question-input-scrollable { - flex: 1; + flex: 0 1 auto; min-height: 0; overscroll-behavior: contain; } -/* tab bar for multi-question carousels */ -.interactive-session .chat-question-carousel-container .chat-question-tab-bar { - display: flex; - align-items: center; - gap: 2px; - padding: 4px 8px 4px 4px; - border-bottom: 1px solid var(--vscode-chat-requestBorder); - background: var(--vscode-chat-list-background); - - .chat-question-tabs { - display: flex; - align-items: center; - gap: 2px; - flex: 1; - min-width: 0; - overflow-x: auto; - } - - .chat-question-tab { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 2px 10px 2px 8px; - border-radius: var(--vscode-cornerRadius-medium); - font-size: var(--vscode-chat-font-size-body-s); - cursor: pointer; - font-weight: 500; - white-space: nowrap; - user-select: none; - color: var(--vscode-descriptionForeground); - outline: none; - } +/* close button for multi-question carousels (positioned top-right) */ +.interactive-session .chat-question-carousel-container > .chat-question-close-container { + position: absolute; + top: 6px; + right: 8px; + z-index: 1; - .chat-question-tab .chat-question-tab-indicator { - font-size: 10px; - line-height: 1; - } - - .chat-question-tab .chat-question-tab-indicator.codicon-circle-filled { - color: var(--vscode-textLink-foreground); + .monaco-button.chat-question-close { + min-width: 22px; + width: 22px; + height: 22px; + padding: 0; + border: none !important; + box-shadow: none !important; + background: transparent !important; + color: var(--vscode-icon-foreground) !important; } - .chat-question-tab.no-icon { - padding: 2px 8px; + .monaco-button.chat-question-close:hover:not(.disabled) { + background: var(--vscode-toolbar-hoverBackground) !important; } +} - .chat-question-tab:hover { - background: var(--vscode-list-hoverBackground); - color: var(--vscode-foreground); - } +/* footer with nav arrows, step indicator, and submit */ +.interactive-session .chat-question-carousel-container .chat-question-footer-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 8px; + border-top: 1px solid var(--vscode-chat-requestBorder); + flex-shrink: 0; - .chat-question-tab.active { - background: var(--vscode-list-activeSelectionBackground); - color: var(--vscode-list-activeSelectionForeground); + .chat-question-footer-left { + display: flex; + align-items: center; + gap: 8px; } - .chat-question-tab:focus-visible { - outline: none; + .chat-question-footer-right { + display: flex; + align-items: center; + gap: 8px; } - .chat-question-tab-controls { + .chat-question-nav-arrows { display: flex; align-items: center; gap: 4px; - flex-shrink: 0; - margin-left: auto; } - .chat-question-tab-controls .monaco-button.chat-question-submit-button { - background: var(--vscode-button-background) !important; - color: var(--vscode-button-foreground) !important; + .monaco-button.chat-question-nav-arrow { + min-width: 22px; + width: 22px; height: 22px; - min-width: auto; - padding: 0 8px; + padding: 0; + border: none !important; + box-shadow: none !important; + background: transparent !important; + color: var(--vscode-foreground) !important; } - .chat-question-tab-controls .monaco-button.chat-question-submit-button:hover:not(.disabled) { - background: var(--vscode-button-hoverBackground) !important; + .monaco-button.chat-question-nav-arrow:hover:not(.disabled) { + background: var(--vscode-toolbar-hoverBackground) !important; } - .chat-question-close-container { - flex-shrink: 0; - - .monaco-button.chat-question-close { - min-width: 22px; - width: 22px; - height: 22px; - padding: 0; - border: none !important; - box-shadow: none !important; - background: transparent !important; - color: var(--vscode-foreground) !important; - } - - .monaco-button.chat-question-close:hover:not(.disabled) { - background: var(--vscode-toolbar-hoverBackground) !important; - } + .monaco-button.chat-question-nav-arrow.disabled { + opacity: 0.4; } -} -/* footer with submit and cancel buttons */ -.interactive-session .chat-question-carousel-container .chat-question-footer-row { - display: flex; - justify-content: flex-end; - align-items: center; - gap: 8px; - padding: 4px 8px; - border-top: 1px solid var(--vscode-chat-requestBorder); - background: var(--vscode-chat-list-background); + .chat-question-step-indicator { + font-size: var(--vscode-chat-font-size-body-s); + color: var(--vscode-descriptionForeground); + } .chat-question-submit-hint { font-size: 11px; @@ -471,14 +412,6 @@ .monaco-button.chat-question-submit-button:hover:not(.disabled) { background: var(--vscode-button-hoverBackground) !important; } - - .monaco-button.chat-question-cancel-button { - height: 22px; - width: auto; - flex: 0 0 auto; - min-width: auto; - padding: 0 8px; - } } /* summary (after finished) */ @@ -486,7 +419,7 @@ display: flex; flex-direction: column; gap: 8px; - padding: 16px; + padding: 8px; .chat-question-summary-item { display: flex; @@ -501,26 +434,22 @@ overflow-wrap: break-word; } - .chat-question-summary-answer { + .chat-question-summary-answer-title { color: var(--vscode-foreground); + font-weight: 600; word-wrap: break-word; overflow-wrap: break-word; } - .chat-question-summary-skipped { - color: var(--vscode-descriptionForeground); - font-style: italic; - font-size: var(--vscode-chat-font-size-body-s); - } - - .chat-question-summary-empty { - color: var(--vscode-descriptionForeground); - font-size: var(--vscode-chat-font-size-body-s); - padding: 0; + .chat-question-summary-answer-desc { + color: var(--vscode-foreground); + word-wrap: break-word; + overflow-wrap: break-word; } - .chat-question-summary-unanswered { + .chat-question-summary-skipped { color: var(--vscode-descriptionForeground); font-style: italic; + font-size: var(--vscode-chat-font-size-body-s); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index b559869616fec..efd1ff467d614 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -64,7 +64,7 @@ import { IChatRequestVariableEntry } from '../../common/attachments/chatVariable import { IChatChangesSummaryPart, IChatCodeCitations, IChatErrorDetailsPart, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, isRequestVM, isResponseVM, IChatPendingDividerViewModel, isPendingDividerVM } from '../../common/model/chatViewModel.js'; import { getNWords } from '../../common/model/chatWordCounter.js'; import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind, CollapsedToolsDisplayMode, ThinkingDisplayMode } from '../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel, CollapsedToolsDisplayMode, ThinkingDisplayMode } from '../../common/constants.js'; import { ClickAnimation } from '../../../../../base/browser/ui/animations/animations.js'; import { MarkHelpfulActionId, MarkUnhelpfulActionId } from '../actions/chatTitleActions.js'; import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidgetService } from '../chat.js'; @@ -2335,7 +2335,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - if (!shouldAutoReply) { + // always autoreply in autopilot mode. + const isAutopilot = isResponseVM(context.element) && context.element.model.request?.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot; + if (!shouldAutoReply && !isAutopilot) { // Roll back the in-progress mark if auto-reply is not enabled. if (stableKey) { this._autoRepliedQuestionCarousels.delete(stableKey); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 8a391fa71d69b..eb97584dd419b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -64,7 +64,7 @@ import { IChatTodoListService } from '../../common/tools/chatTodoListService.js' import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isWorkspaceVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js'; import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel } from '../../common/constants.js'; import { ILanguageModelToolsService, isToolSet } from '../../common/tools/languageModelToolsService.js'; import { ComputeAutomaticInstructions } from '../../common/promptSyntax/computeAutomaticInstructions.js'; import { IHandOff, PromptHeader } from '../../common/promptSyntax/promptFileParser.js'; @@ -1552,6 +1552,7 @@ export class ChatWidget extends Disposable implements IChatWidget { rowContainer.appendChild(this.inputContainer); this.createInput(this.inputContainer); this.input.setChatMode(this.inputPart.currentModeObs.get().id); + this.input.setPermissionLevel(this.inputPart.currentModeInfo.permissionLevel ?? ChatPermissionLevel.Default); this.input.setEditing(true, isEditingSentRequest); this._onDidChangeActiveInputEditor.fire(); } else { @@ -1652,6 +1653,7 @@ export class ChatWidget extends Disposable implements IChatWidget { if (!isInput) { this.inputPart.setChatMode(this.input.currentModeObs.get().id); + this.inputPart.setPermissionLevel(this.input.currentModeInfo.permissionLevel ?? ChatPermissionLevel.Default); const currentModel = this.input.selectedLanguageModel.get(); if (currentModel) { this.inputPart.switchModel(currentModel.metadata); @@ -1735,6 +1737,7 @@ export class ChatWidget extends Disposable implements IChatWidget { defaultMode: this.viewOptions.defaultMode, sessionTypePickerDelegate: this.viewOptions.sessionTypePickerDelegate, workspacePickerDelegate: this.viewOptions.workspacePickerDelegate, + isSessionsWindow: this.viewOptions.isSessionsWindow, }; if (this.viewModel?.editing) { @@ -2139,7 +2142,8 @@ export class ChatWidget extends Disposable implements IChatWidget { const options: IChatSendRequestOptions = { attempt: lastRequest.attempt + 1, location: this.location, - userSelectedModelId: this.input.currentLanguageModel + userSelectedModelId: this.input.currentLanguageModel, + modeInfo: this.input.currentModeInfo, }; return await this.chatService.resendRequest(lastRequest, options); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index f4a5f39c53f12..db90c77372dcd 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -85,16 +85,17 @@ import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEnt import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js'; import { IChatFollowup, IChatQuestionCarousel, IChatService, IChatSessionContext } from '../../../common/chatService/chatService.js'; import { agentOptionId, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsService, isIChatSessionFileChange2, localChatSessionType } from '../../../common/chatSessionsService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind, validateChatMode } from '../../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel, validateChatMode } from '../../../common/constants.js'; import { IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../../common/languageModels.js'; import { IChatModelInputState, IChatRequestModeInfo, IInputModel } from '../../../common/model/chatModel.js'; +import { filterModelsForSession, findDefaultModel, hasModelsTargetingSession, isModelValidForSession, mergeModelsWithCache, resolveModelFromSyncState, shouldResetModelToDefault, shouldResetOnModelListChange, shouldRestoreLateArrivingModel, shouldRestorePersistedModel } from './chatModelSelectionLogic.js'; import { getChatSessionType } from '../../../common/model/chatUri.js'; import { IChatResponseViewModel, isResponseVM } from '../../../common/model/chatViewModel.js'; import { IChatAgentService } from '../../../common/participants/chatAgents.js'; import { ILanguageModelToolsService } from '../../../common/tools/languageModelToolsService.js'; import { ChatHistoryNavigator } from '../../../common/widget/chatWidgetHistoryService.js'; -import { ChatSessionPrimaryPickerAction, ChatSubmitAction, IChatExecuteActionContext, OpenDelegationPickerAction, OpenModelPickerAction, OpenModePickerAction, OpenSessionTargetPickerAction, OpenWorkspacePickerAction } from '../../actions/chatExecuteActions.js'; +import { ChatSessionPrimaryPickerAction, ChatSubmitAction, IChatExecuteActionContext, OpenDelegationPickerAction, OpenModelPickerAction, OpenModePickerAction, OpenPermissionPickerAction, OpenSessionTargetPickerAction, OpenWorkspacePickerAction } from '../../actions/chatExecuteActions.js'; import { AgentSessionProviders, getAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { IAgentSessionsService } from '../../agentSessions/agentSessionsService.js'; import { ChatAttachmentModel } from '../../attachments/chatAttachmentModel.js'; @@ -121,6 +122,7 @@ import { ChatSelectedTools } from './chatSelectedTools.js'; import { DelegationSessionPickerActionItem } from './delegationSessionPickerActionItem.js'; import { IModelPickerDelegate } from './modelPickerActionItem.js'; import { IModePickerDelegate, ModePickerActionItem } from './modePickerActionItem.js'; +import { IPermissionPickerDelegate, PermissionPickerActionItem } from './permissionPickerActionItem.js'; import { SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; import { WorkspacePickerActionItem } from './workspacePickerActionItem.js'; import { ChatContextUsageWidget } from '../../widgetHosts/viewPane/chatContextUsageWidget.js'; @@ -168,6 +170,11 @@ export interface IChatInputPartOptions { * for their chat request. This is useful for empty window contexts. */ workspacePickerDelegate?: IWorkspacePickerDelegate; + /** + * Whether we are running in the sessions window. + * When true, the secondary toolbar (permissions picker) is hidden. + */ + isSessionsWindow?: boolean; } export interface IWorkingSetEntry { @@ -284,6 +291,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private container!: HTMLElement; private inputSideToolbarContainer?: HTMLElement; + private secondaryToolbarContainer!: HTMLElement; + private secondaryToolbar!: MenuWorkbenchToolBar; private followupsContainer!: HTMLElement; private readonly followupsDisposables: DisposableStore = this._register(new DisposableStore()); @@ -365,6 +374,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private chatSessionHasTargetedModels: IContextKey; private modelWidget: EnhancedModelPickerActionItem | undefined; private modeWidget: ModePickerActionItem | undefined; + private permissionWidget: PermissionPickerActionItem | undefined; private sessionTargetWidget: SessionTypePickerActionItem | undefined; private delegationWidget: DelegationSessionPickerActionItem | undefined; private chatSessionPickerWidgets: Map = new Map(); @@ -399,6 +409,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge readonly onDidChangeCurrentChatMode: Event = this._onDidChangeCurrentChatMode.event; private readonly _currentModeObservable: ISettableObservable; + private readonly _currentPermissionLevel: ISettableObservable; + private permissionLevelKey: IContextKey; public get currentModeKind(): ChatModeKind { const mode = this._currentModeObservable.get(); @@ -411,6 +423,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this._currentModeObservable; } + public get currentPermissionLevelObs(): IObservable { + return this._currentPermissionLevel; + } + public get currentModeInfo(): IChatRequestModeInfo { const mode = this._currentModeObservable.get(); const modeId: 'ask' | 'agent' | 'edit' | 'custom' | undefined = mode.isBuiltin ? this.currentModeKind : 'custom'; @@ -428,6 +444,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } : undefined, modeId: modeId, applyCodeBlockSuggestionId: undefined, + permissionLevel: this._currentPermissionLevel.get(), }; } @@ -528,6 +545,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event })); this._currentModeObservable = observableValue('currentMode', this.options.defaultMode ?? ChatMode.Agent); + this._currentPermissionLevel = observableValue('permissionLevel', ChatPermissionLevel.Default); this._register(this.editorService.onDidActiveEditorChange(() => { this._indexOfLastOpenedContext = -1; this.refreshChatSessionPickers(); @@ -579,6 +597,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.chatModeKindKey = ChatContextKeys.chatModeKind.bindTo(contextKeyService); this.chatModeNameKey = ChatContextKeys.chatModeName.bindTo(contextKeyService); this.chatModelIdKey = ChatContextKeys.chatModelId.bindTo(contextKeyService); + this.permissionLevelKey = ChatContextKeys.chatPermissionLevel.bindTo(contextKeyService); this.withinEditSessionKey = ChatContextKeys.withinEditSessionDiff.bindTo(contextKeyService); this.filePartOfEditSessionKey = ChatContextKeys.filePartOfEditSession.bindTo(contextKeyService); this.chatSessionHasOptions = ChatContextKeys.chatSessionHasModels.bindTo(contextKeyService); @@ -625,8 +644,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.initSelectedModel(); this._register(this.languageModelsService.onDidChangeLanguageModels(() => { - const selectedModel = this._currentLanguageModel ? this.getModels().find(m => m.identifier === this._currentLanguageModel.get()?.identifier) : undefined; - if (!this.currentLanguageModel || !selectedModel) { + if (shouldResetOnModelListChange(this._currentLanguageModel.get()?.identifier, this.getModels())) { this.setCurrentLanguageModelToDefault(); } })); @@ -719,25 +737,20 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const persistedAsDefault = this.storageService.getBoolean(this.getSelectedModelIsDefaultStorageKey(), StorageScope.APPLICATION, true); if (persistedSelection) { - const model = this.getModels().find(m => m.identifier === persistedSelection); - if (model) { - // Only restore the model if it wasn't the default at the time of storing or it is now the default - if (!persistedAsDefault || model.metadata.isDefaultForLocation[this.location]) { - this.setCurrentLanguageModel(model); - this.checkModelSupported(); - } - } else { + const result = shouldRestorePersistedModel(persistedSelection, persistedAsDefault, this.getModels(), this.location); + if (result.shouldRestore && result.model) { + this.setCurrentLanguageModel(result.model); + this.checkModelSupported(); + } else if (!result.model) { this._waitForPersistedLanguageModel.value = this.languageModelsService.onDidChangeLanguageModels(e => { const persistedModel = this.languageModelsService.lookupLanguageModel(persistedSelection); if (persistedModel) { this._waitForPersistedLanguageModel.clear(); - // Only restore the model if it wasn't the default at the time of storing or it is now the default - if (!persistedAsDefault || persistedModel.isDefaultForLocation[this.location]) { - if (persistedModel.isUserSelectable) { - this.setCurrentLanguageModel({ metadata: persistedModel, identifier: persistedSelection }); - this.checkModelSupported(); - } + const lateModel = { metadata: persistedModel, identifier: persistedSelection }; + if (shouldRestoreLateArrivingModel(persistedSelection, persistedAsDefault, lateModel, this.location)) { + this.setCurrentLanguageModel(lateModel); + this.checkModelSupported(); } } else { this.setCurrentLanguageModelToDefault(); @@ -795,6 +808,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.modeWidget?.show(); } + public openPermissionPicker(): void { + this.permissionWidget?.show(); + } + + public setPermissionLevel(level: ChatPermissionLevel): void { + this._currentPermissionLevel.set(level, undefined); + this.permissionLevelKey.set(level); + this.permissionWidget?.refresh(); + } + public openSessionTargetPicker(): void { this.sessionTargetWidget?.show(); } @@ -881,6 +904,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.selectedToolsModel.resetSessionEnablementState(); this._chatSessionIsEmpty = chatSessionIsEmpty; + // Reset permission level to default on new sessions + if (chatSessionIsEmpty) { + this._currentPermissionLevel.set(ChatPermissionLevel.Default, undefined); + this.permissionLevelKey.set(ChatPermissionLevel.Default); + this.permissionWidget?.refresh(); + } + // TODO@roblourens This is for an experiment which will be obsolete in a month or two and can then be removed. if (chatSessionIsEmpty) { this._setEmptyModelState(); @@ -946,14 +976,18 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Sync selected model - validate it belongs to the current session's model pool if (state?.selectedModel) { - const lm = this._currentLanguageModel.get(); - if (!lm || lm.identifier !== state.selectedModel.identifier) { - if (this.isModelValidForCurrentSession(state.selectedModel)) { - this.setCurrentLanguageModel(state.selectedModel); - } else { - // Model from state doesn't belong to this session's pool - use default - this.setCurrentLanguageModelToDefault(); - } + const allModels = this.getAllMergedModels(); + const sessionType = this.getCurrentSessionType(); + const syncResult = resolveModelFromSyncState(state.selectedModel, this._currentLanguageModel.get(), allModels, sessionType, { + location: this.location, + currentModeKind: this.currentModeKind, + isInlineChatV2Enabled: !!this.configurationService.getValue(InlineChatConfigKeys.EnableV2), + sessionType, + }); + if (syncResult.action === 'apply') { + this.setCurrentLanguageModel(state.selectedModel); + } else if (syncResult.action === 'default') { + this.setCurrentLanguageModelToDefault(); } } @@ -1019,7 +1053,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private checkModelSupported(): void { const lm = this._currentLanguageModel.get(); - if (lm && (!this.modelSupportedForDefaultAgent(lm) || !this.modelSupportedForInlineChat(lm) || !this.isModelValidForCurrentSession(lm))) { + const allModels = this.getAllMergedModels(); + if (shouldResetModelToDefault(lm, this.getModels(), { + location: this.location, + currentModeKind: this.currentModeKind, + isInlineChatV2Enabled: !!this.configurationService.getValue(InlineChatConfigKeys.EnableV2), + sessionType: this.getCurrentSessionType(), + }, allModels)) { this.setCurrentLanguageModelToDefault(); } } @@ -1051,56 +1091,29 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._syncInputStateToModel(); } - private modelSupportedForDefaultAgent(model: ILanguageModelChatMetadataAndIdentifier): boolean { - // Probably this logic could live in configuration on the agent, or somewhere else, if it gets more complex - if (this.currentModeKind === ChatModeKind.Agent) { - return ILanguageModelChatMetadata.suitableForAgentMode(model.metadata); - } - - return true; - } - - private modelSupportedForInlineChat(model: ILanguageModelChatMetadataAndIdentifier): boolean { - if (this.location !== ChatAgentLocation.EditorInline || !this.configurationService.getValue(InlineChatConfigKeys.EnableV2)) { - return true; - } - return !!model.metadata.capabilities?.toolCalling; - } - - private getModels(): ILanguageModelChatMetadataAndIdentifier[] { + /** + * Get all models merged from live and cache, without session/mode filtering. + * This is the canonical source for the full model pool, including cached models + * that bridge startup races when live models haven't loaded yet. + */ + private getAllMergedModels(): ILanguageModelChatMetadataAndIdentifier[] { const cachedModels = this.storageService.getObject(CachedLanguageModelsKey, StorageScope.APPLICATION, []); const liveModels = this.languageModelsService.getLanguageModelIds() .map(modelId => ({ identifier: modelId, metadata: this.languageModelsService.lookupLanguageModel(modelId)! })); - // Merge live models with cached models per-vendor. For vendors whose - // models have resolved, use the live data. For vendors that are still - // contributed but haven't resolved yet (startup race), keep their - // cached models. Vendors that are no longer contributed at all (e.g. - // extension uninstalled) are evicted from the cache. - let models: ILanguageModelChatMetadataAndIdentifier[]; + const contributedVendors = new Set(this.languageModelsService.getVendors().map(v => v.vendor)); + const models = mergeModelsWithCache(liveModels, cachedModels, contributedVendors); if (liveModels.length > 0) { - const liveVendors = new Set(liveModels.map(m => m.metadata.vendor)); - const contributedVendors = new Set(this.languageModelsService.getVendors().map(v => v.vendor)); - models = [ - ...liveModels, - ...cachedModels.filter(m => !liveVendors.has(m.metadata.vendor) && contributedVendors.has(m.metadata.vendor)), - ]; this.storageService.store(CachedLanguageModelsKey, models, StorageScope.APPLICATION, StorageTarget.MACHINE); - } else { - models = cachedModels; } - models.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)); + return models; + } - const sessionType = this.getCurrentSessionType(); - if (sessionType && sessionType !== AgentSessionProviders.Local) { - // Session has a specific chat session type - show only models that target - // this session type, if any such models exist. - return models.filter(entry => entry.metadata?.targetChatSessionType === sessionType && entry.metadata?.isUserSelectable); - } + private getModels(): ILanguageModelChatMetadataAndIdentifier[] { + const models = this.getAllMergedModels(); + models.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)); - // No session type or no targeted models - show general models (those without - // a targetChatSessionType) filtered by the standard criteria. - return models.filter(entry => !entry.metadata?.targetChatSessionType && entry.metadata?.isUserSelectable && this.modelSupportedForDefaultAgent(entry) && this.modelSupportedForInlineChat(entry)); + return filterModelsForSession(models, this.getCurrentSessionType(), this.currentModeKind, this.location, !!this.configurationService.getValue(InlineChatConfigKeys.EnableV2)); } /** @@ -1122,28 +1135,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge * This is used to set the context key that controls model picker visibility. */ private hasModelsTargetingSessionType(): boolean { - const sessionType = this.getCurrentSessionType(); - if (!sessionType) { - return false; - } - return this.languageModelsService.getLanguageModelIds().some(modelId => { - const metadata = this.languageModelsService.lookupLanguageModel(modelId); - return metadata?.targetChatSessionType === sessionType; - }); + return hasModelsTargetingSession(this.getAllMergedModels(), this.getCurrentSessionType()); } - /** - * Check if a model is valid for the current session's model pool. - * If the session has targeted models, the model must target this session type. - * If no models target this session, the model must not have a targetChatSessionType. - */ private isModelValidForCurrentSession(model: ILanguageModelChatMetadataAndIdentifier): boolean { - if (this.hasModelsTargetingSessionType()) { - // Session has targeted models - model must match - return model.metadata.targetChatSessionType === this.getCurrentSessionType(); - } - // No targeted models - model must not be session-specific - return !model.metadata.targetChatSessionType; + return isModelValidForSession(model, this.getAllMergedModels(), this.getCurrentSessionType()); } /** @@ -1218,7 +1214,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private setCurrentLanguageModelToDefault() { const allModels = this.getModels(); - const defaultModel = allModels.find(m => m.metadata.isDefaultForLocation[this.location]) || allModels[0]; + const defaultModel = findDefaultModel(allModels, this.location); if (defaultModel) { this.setCurrentLanguageModel(defaultModel); } @@ -1970,11 +1966,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [ dom.h('.chat-input-container@inputContainer', [ dom.h('.chat-editor-container@editorContainer'), - dom.h('.chat-input-toolbars@inputToolbars', [ - dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), - ]), + dom.h('.chat-input-toolbars@inputToolbars'), ]), ]), + dom.h('.chat-secondary-toolbar@secondaryToolbar', [ + dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), + ]), dom.h('.chat-attachments-container@attachmentsContainer', [ dom.h('.chat-attached-context@attachedContextContainer'), ]), @@ -1995,11 +1992,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.h('.chat-attached-context@attachedContextContainer'), ]), dom.h('.chat-editor-container@editorContainer'), - dom.h('.chat-input-toolbars@inputToolbars', [ - dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), - ]), + dom.h('.chat-input-toolbars@inputToolbars'), ]), ]), + dom.h('.chat-secondary-toolbar@secondaryToolbar', [ + dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), + ]), ]); } this.container = elements.root; @@ -2020,6 +2018,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.attachmentsContainer = elements.attachmentsContainer; this.attachedContextContainer = elements.attachedContextContainer; const toolbarsContainer = elements.inputToolbars; + this.secondaryToolbarContainer = elements.secondaryToolbar; + if (this.options.isSessionsWindow) { + this.secondaryToolbarContainer.style.display = 'none'; + } this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; this.chatInputTodoListWidgetContainer = elements.chatInputTodoListWidgetContainer; this.chatGettingStartedTipContainer = elements.chatGettingStartedTipContainer; @@ -2028,6 +2030,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.chatInputWidgetsContainer = elements.chatInputWidgetsContainer; this.contextUsageWidgetContainer = elements.contextUsageWidgetContainer; + if (this.options.isSessionsWindow) { + toolbarsContainer.prepend(this.contextUsageWidgetContainer); + } + // Context usage widget — will be positioned in the toolbar after toolbars are created this.contextUsageWidget = this._register(this.instantiationService.createInstance(ChatContextUsageWidget)); this.contextUsageWidgetContainer.appendChild(this.contextUsageWidget.domNode); @@ -2279,7 +2285,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // eslint-disable-next-line no-restricted-syntax const container = toolbarElement.querySelector('.chat-sessionPicker-container'); this.chatSessionPickerContainer = container as HTMLElement | undefined; - if (this.cachedWidth && typeof this.cachedInputToolbarWidth === 'number' && this.cachedInputToolbarWidth !== this.inputActionsToolbar.getItemsWidth()) { this._toolbarRelayoutScheduler.schedule(); } @@ -2312,6 +2317,62 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge toolbarSide.context = { widget } satisfies IChatExecuteActionContext; } + // Secondary toolbar (permissions) — below the input box + this.secondaryToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, this.secondaryToolbarContainer, MenuId.ChatInputSecondary, { + telemetrySource: this.options.menus.telemetrySource, + menuOptions: { shouldForwardArgs: true }, + hiddenItemStrategy: HiddenItemStrategy.NoHide, + hoverDelegate, + actionViewItemProvider: (action, options) => { + if ((action.id === OpenSessionTargetPickerAction.ID || action.id === OpenDelegationPickerAction.ID) && action instanceof MenuItemAction) { + const getActiveSessionType = () => { + const sessionResource = this._widget?.viewModel?.sessionResource; + return sessionResource ? getAgentSessionProvider(sessionResource) : undefined; + }; + const delegate: ISessionTypePickerDelegate = this.options.sessionTypePickerDelegate ?? { + getActiveSessionProvider: () => { + return getActiveSessionType(); + }, + getPendingDelegationTarget: () => { + return this._pendingDelegationTarget; + }, + setPendingDelegationTarget: (provider: AgentSessionProviders) => { + const isActive = getActiveSessionType() === provider; + this._pendingDelegationTarget = isActive ? undefined : provider; + this.updateWidgetLockStateFromSessionType(provider); + this.updateAgentSessionTypeContextKey(); + this.refreshChatSessionPickers(); + }, + }; + const isWelcomeViewMode = !!this.options.sessionTypePickerDelegate?.setActiveSessionProvider; + const Picker = (action.id === OpenSessionTargetPickerAction.ID || isWelcomeViewMode) ? SessionTypePickerActionItem : DelegationSessionPickerActionItem; + return this.sessionTargetWidget = this.instantiationService.createInstance(Picker, action, location === ChatWidgetLocation.Editor ? 'editor' : 'sidebar', delegate, pickerOptions); + } else if (action.id === OpenWorkspacePickerAction.ID && action instanceof MenuItemAction) { + if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY && this.options.workspacePickerDelegate) { + return this.instantiationService.createInstance(WorkspacePickerActionItem, action, this.options.workspacePickerDelegate, pickerOptions); + } else { + const empty = new BaseActionViewItem(undefined, action); + if (empty.element) { + empty.element.style.display = 'none'; + } + return empty; + } + } else if (action.id === OpenPermissionPickerAction.ID && action instanceof MenuItemAction) { + const delegate: IPermissionPickerDelegate = { + currentPermissionLevel: this._currentPermissionLevel, + setPermissionLevel: (level: ChatPermissionLevel) => { + this._currentPermissionLevel.set(level, undefined); + this.permissionLevelKey.set(level); + }, + }; + return this.permissionWidget = this.instantiationService.createInstance(PermissionPickerActionItem, action, delegate, pickerOptions); + } + return undefined; + } + })); + this.secondaryToolbar.getElement().classList.add('chat-secondary-input-toolbar'); + this.secondaryToolbar.context = { widget } satisfies IChatExecuteActionContext; + let inputModel = this.modelService.getModel(this.inputUri); if (!inputModel) { inputModel = this.modelService.createModel('', null, this.inputUri, true); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index 0257f20252dd0..2011b0f95e917 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -655,9 +655,7 @@ export class ModelPickerWidget extends Disposable { domChildren.push(this._badgeIcon); } - if (!this._hideChevrons?.get()) { - domChildren.push(...renderLabelWithIcons(`$(chevron-down)`)); - } + domChildren.push(...renderLabelWithIcons(`$(chevron-down)`)); dom.reset(this._domNode, ...domChildren); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts new file mode 100644 index 0000000000000..62dbca81dc303 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelSelectionLogic.ts @@ -0,0 +1,290 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ChatAgentLocation, ChatModeKind } from '../../../common/constants.js'; +import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier } from '../../../common/languageModels.js'; + +/** + * Describes the context needed for model selection decisions. + */ +export interface IModelSelectionContext { + readonly location: ChatAgentLocation; + readonly currentModeKind: ChatModeKind; + readonly isInlineChatV2Enabled: boolean; + readonly sessionType: string | undefined; +} + +/** + * Filter models based on session type. + * When a session has a specific type (and it's not 'local'), only models targeting that + * session type are returned. Otherwise, general-purpose models are returned. + */ +export function filterModelsForSession( + models: ILanguageModelChatMetadataAndIdentifier[], + sessionType: string | undefined, + currentModeKind: ChatModeKind, + location: ChatAgentLocation, + isInlineChatV2Enabled: boolean, +): ILanguageModelChatMetadataAndIdentifier[] { + if (sessionType && sessionType !== 'local' && hasModelsTargetingSession(models, sessionType)) { + return models.filter(entry => + entry.metadata?.targetChatSessionType === sessionType && + entry.metadata?.isUserSelectable && + isModelSupportedForMode(entry, currentModeKind) && + isModelSupportedForInlineChat(entry, location, isInlineChatV2Enabled) + ); + } + + return models.filter(entry => + !entry.metadata?.targetChatSessionType && + entry.metadata?.isUserSelectable && + isModelSupportedForMode(entry, currentModeKind) && + isModelSupportedForInlineChat(entry, location, isInlineChatV2Enabled) + ); +} + +/** + * Check if a model is suitable for the current chat mode (e.g., agent mode requires tool calling). + */ +export function isModelSupportedForMode( + model: ILanguageModelChatMetadataAndIdentifier, + currentModeKind: ChatModeKind, +): boolean { + if (currentModeKind === ChatModeKind.Agent) { + return ILanguageModelChatMetadata.suitableForAgentMode(model.metadata); + } + return true; +} + +/** + * Check if a model is suitable for inline chat (editor inline) usage. + */ +export function isModelSupportedForInlineChat( + model: ILanguageModelChatMetadataAndIdentifier, + location: ChatAgentLocation, + isInlineChatV2Enabled: boolean, +): boolean { + if (location !== ChatAgentLocation.EditorInline || !isInlineChatV2Enabled) { + return true; + } + return !!model.metadata.capabilities?.toolCalling; +} + +/** + * Check if any models in the pool target a specific session type. + */ +export function hasModelsTargetingSession( + allModels: ILanguageModelChatMetadataAndIdentifier[], + sessionType: string | undefined, +): boolean { + if (!sessionType) { + return false; + } + return allModels.some(m => m.metadata.targetChatSessionType === sessionType); +} + +/** + * Check if a model is valid for the current session's model pool. + * If the session has targeted models, the model must target that session type. + * If no models target this session, the model must not be session-specific. + */ +export function isModelValidForSession( + model: ILanguageModelChatMetadataAndIdentifier, + allModels: ILanguageModelChatMetadataAndIdentifier[], + sessionType: string | undefined, +): boolean { + if (hasModelsTargetingSession(allModels, sessionType)) { + return model.metadata.targetChatSessionType === sessionType; + } + return !model.metadata.targetChatSessionType; +} + +/** + * Find the default model for a given location from a list of models. + * Prefers the model marked as default for the location, falls back to the first model. + */ +export function findDefaultModel( + models: ILanguageModelChatMetadataAndIdentifier[], + location: ChatAgentLocation, +): ILanguageModelChatMetadataAndIdentifier | undefined { + return models.find(m => m.metadata.isDefaultForLocation[location]) || models[0]; +} + +/** + * Determine whether a persisted model selection should be restored. + * + * A persisted model should be restored if: + * 1. The model still exists in the available models list + * 2. Either the model wasn't the default at the time it was persisted, + * OR it is currently the default for the location + * + * This prevents scenarios where a user's explicit model choice gets overridden + * when the default model changes, while still tracking default model changes + * for users who never explicitly chose a model. + */ +export function shouldRestorePersistedModel( + persistedModelId: string, + persistedAsDefault: boolean, + availableModels: ILanguageModelChatMetadataAndIdentifier[], + location: ChatAgentLocation, +): { shouldRestore: boolean; model: ILanguageModelChatMetadataAndIdentifier | undefined } { + const model = availableModels.find(m => m.identifier === persistedModelId); + if (!model) { + return { shouldRestore: false, model: undefined }; + } + + if (!persistedAsDefault || model.metadata.isDefaultForLocation[location]) { + return { shouldRestore: true, model }; + } + + return { shouldRestore: false, model }; +} + +/** + * Determines whether the current model should be reset because it is no longer + * compatible with the current mode, session, or availability. + * + * Returns true if the model should be reset to default. + */ +export function shouldResetModelToDefault( + currentModel: ILanguageModelChatMetadataAndIdentifier | undefined, + availableModels: ILanguageModelChatMetadataAndIdentifier[], + context: IModelSelectionContext, + allModels: ILanguageModelChatMetadataAndIdentifier[], +): boolean { + if (!currentModel) { + return true; + } + + // Model is no longer in the available list + if (!availableModels.some(m => m.identifier === currentModel.identifier)) { + return true; + } + + // Model not supported for current mode + if (!isModelSupportedForMode(currentModel, context.currentModeKind)) { + return true; + } + + // Model not supported for inline chat + if (!isModelSupportedForInlineChat(currentModel, context.location, context.isInlineChatV2Enabled)) { + return true; + } + + // Model not valid for current session + if (!isModelValidForSession(currentModel, allModels, context.sessionType)) { + return true; + } + + return false; +} + +/** + * Determines whether a model from a sync state should be applied to the current view. + * + * Returns an action: + * - `'keep'` - the view already has the same model; no change needed. + * - `'apply'` - the state model is valid; the caller should switch to it. + * - `'default'` - the state model is incompatible (wrong session pool, unsupported + * mode, or missing inline-chat capability); the caller should fall + * back to the default model for the current location. + * + * @param context Optional because some callers (e.g. unit tests, or code paths + * that only care about session-pool validation) don't have a full UI context + * available. When omitted, mode and inline-chat checks are skipped and only + * session-pool membership is validated. + */ +export function resolveModelFromSyncState( + stateModel: ILanguageModelChatMetadataAndIdentifier, + currentModel: ILanguageModelChatMetadataAndIdentifier | undefined, + allModels: ILanguageModelChatMetadataAndIdentifier[], + sessionType: string | undefined, + context?: IModelSelectionContext, +): { action: 'keep' | 'apply' | 'default' } { + // Already the same model — nothing to do + if (currentModel && currentModel.identifier === stateModel.identifier) { + return { action: 'keep' }; + } + + // Validate the state model belongs to this session's model pool + if (!isModelValidForSession(stateModel, allModels, sessionType)) { + return { action: 'default' }; + } + + // When a UI context is available, also validate mode and inline-chat compatibility + if (context) { + if (!isModelSupportedForMode(stateModel, context.currentModeKind)) { + return { action: 'default' }; + } + if (!isModelSupportedForInlineChat(stateModel, context.location, context.isInlineChatV2Enabled)) { + return { action: 'default' }; + } + } + + return { action: 'apply' }; +} + +/** + * Merges live models with cached models per-vendor. + * For vendors whose models have resolved, uses live data. + * For vendors that are contributed but haven't resolved yet (startup race), keeps cached models. + * Vendors no longer contributed are evicted from cache. + */ +export function mergeModelsWithCache( + liveModels: ILanguageModelChatMetadataAndIdentifier[], + cachedModels: ILanguageModelChatMetadataAndIdentifier[], + contributedVendors: Set, +): ILanguageModelChatMetadataAndIdentifier[] { + if (liveModels.length > 0) { + const liveVendors = new Set(liveModels.map(m => m.metadata.vendor)); + return [ + ...liveModels, + ...cachedModels.filter(m => !liveVendors.has(m.metadata.vendor) && contributedVendors.has(m.metadata.vendor)), + ]; + } + return cachedModels; +} + +/** + * Determines whether the currently selected model should be reset to default + * when the language model list changes. + * + * Returns true if the model should be reset to default (i.e., the selected model + * is no longer in the available models list). + */ +export function shouldResetOnModelListChange( + currentModelId: string | undefined, + availableModels: ILanguageModelChatMetadataAndIdentifier[], +): boolean { + if (!currentModelId) { + return true; + } + return !availableModels.some(m => m.identifier === currentModelId); +} + +/** + * Determines whether a late-arriving persisted model should be restored. + * This handles the startup race where the model wasn't available during + * `initSelectedModel` but arrives later via `onDidChangeLanguageModels`. + * + * The model must pass both the persisted-default check and the `isUserSelectable` check. + */ +export function shouldRestoreLateArrivingModel( + persistedModelId: string, + persistedAsDefault: boolean, + model: ILanguageModelChatMetadataAndIdentifier, + location: ChatAgentLocation, +): boolean { + if (!model.metadata.isUserSelectable) { + return false; + } + const result = shouldRestorePersistedModel( + persistedModelId, + persistedAsDefault, + [model], + location, + ); + return result.shouldRestore; +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts index c03c6f69f393e..51c0ad382e50c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, addDisposableListener, append, EventType } from '../../../../../../base/browser/dom.js'; +import { $, addDisposableListener, append, EventType, ModifierKeyEmitter } from '../../../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../../../base/browser/keyboardEvent.js'; import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { Action, IAction } from '../../../../../../base/common/actions.js'; @@ -42,6 +42,7 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { private readonly _primaryActionAction: Action; private readonly _primaryAction: ActionViewItem; private readonly _dropdown: ActionWidgetDropdownActionViewItem; + private _altKeyPressed = false; constructor( action: IAction, @@ -61,7 +62,7 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { this._primaryActionAction = this._register(new Action( 'chat.queuePickerPrimary', isSteerDefault ? localize('chat.steerWithMessage', "Steer with Message") : localize('chat.queueMessage', "Add to Queue"), - ThemeIcon.asClassName(Codicon.arrowUp), + ThemeIcon.asClassName(isSteerDefault ? Codicon.arrowRight : Codicon.add), !!contextKeyService.getContextKeyValue(ChatContextKeys.inputHasText.key), () => this._runDefaultAction() )); @@ -91,21 +92,35 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { this._updatePrimaryAction(); } })); + + // Toggle icon when Alt key is pressed/released + this._register(ModifierKeyEmitter.getInstance().event(status => { + if (this._altKeyPressed !== status.altKey) { + this._altKeyPressed = status.altKey; + this._updatePrimaryAction(); + } + })); } private _isSteerDefault(): boolean { return this.configurationService.getValue(ChatConfiguration.RequestQueueingDefaultAction) === 'steer'; } - private _updatePrimaryAction(): void { + private _isEffectiveSteer(): boolean { const isSteerDefault = this._isSteerDefault(); - this._primaryActionAction.label = isSteerDefault + return this._altKeyPressed ? !isSteerDefault : isSteerDefault; + } + + private _updatePrimaryAction(): void { + const isSteer = this._isEffectiveSteer(); + this._primaryActionAction.label = isSteer ? localize('chat.steerWithMessage', "Steer with Message") : localize('chat.queueMessage', "Add to Queue"); + this._primaryActionAction.class = ThemeIcon.asClassName(isSteer ? Codicon.arrowRight : Codicon.add); } private _runDefaultAction(): void { - const actionId = this._isSteerDefault() + const actionId = this._isEffectiveSteer() ? ChatSteerWithMessageAction.ID : ChatQueueMessageAction.ID; this.commandService.executeCommand(actionId); 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 04bfc652890fc..af61812b3a91f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts @@ -209,9 +209,7 @@ export class ModelPickerActionItem extends ChatInputPickerActionViewItem { } domChildren.push(dom.$('span.chat-input-picker-label', undefined, name ?? localize('chat.modelPicker.auto', "Auto"))); - if (!this.pickerOptions.hideChevrons.get()) { - domChildren.push(...renderLabelWithIcons(`$(chevron-down)`)); - } + domChildren.push(...renderLabelWithIcons(`$(chevron-down)`)); dom.reset(element, ...domChildren); this.setAriaLabelAttributes(element); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts new file mode 100644 index 0000000000000..5f0cc0988d2a3 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -0,0 +1,156 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { renderLabelWithIcons } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IObservable } from '../../../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { localize } from '../../../../../../nls.js'; +import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js'; +import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { ChatConfiguration, ChatPermissionLevel } from '../../../common/constants.js'; +import { MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; + +export interface IPermissionPickerDelegate { + readonly currentPermissionLevel: IObservable; + readonly setPermissionLevel: (level: ChatPermissionLevel) => void; +} + +export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { + constructor( + action: MenuItemAction, + private readonly delegate: IPermissionPickerDelegate, + pickerOptions: IChatInputPickerOptions, + @IActionWidgetService actionWidgetService: IActionWidgetService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextKeyService contextKeyService: IContextKeyService, + @ITelemetryService telemetryService: ITelemetryService, + @IConfigurationService configurationService: IConfigurationService, + ) { + const isAutoApprovePolicyRestricted = () => configurationService.inspect(ChatConfiguration.GlobalAutoApprove).policyValue === false; + const isAutopilotEnabled = () => configurationService.getValue(ChatConfiguration.AutopilotEnabled) !== false; + const actionProvider: IActionWidgetDropdownActionProvider = { + getActions: () => { + const currentLevel = delegate.currentPermissionLevel.get(); + const policyRestricted = isAutoApprovePolicyRestricted(); + const actions: IActionWidgetDropdownAction[] = [ + { + ...action, + id: 'chat.permissions.default', + label: localize('permissions.default', "Default Approvals"), + icon: ThemeIcon.fromId(Codicon.shield.id), + checked: currentLevel === ChatPermissionLevel.Default, + tooltip: '', + hover: { + content: localize('permissions.default.description', "Use configured approval settings"), + position: pickerOptions.hoverPosition + }, + run: async () => { + delegate.setPermissionLevel(ChatPermissionLevel.Default); + if (this.element) { + this.renderLabel(this.element); + } + }, + } satisfies IActionWidgetDropdownAction, + { + ...action, + id: 'chat.permissions.autoApprove', + label: localize('permissions.autoApprove', "Bypass Approvals"), + icon: ThemeIcon.fromId(Codicon.warning.id), + checked: currentLevel === ChatPermissionLevel.AutoApprove, + enabled: !policyRestricted, + tooltip: policyRestricted ? localize('permissions.autoApprove.policyDisabled', "Disabled by enterprise policy") : '', + hover: { + content: policyRestricted + ? localize('permissions.autoApprove.policyDescription', "Disabled by enterprise policy") + : localize('permissions.autoApprove.description', "Auto-approve all tool calls and retry on errors"), + position: pickerOptions.hoverPosition + }, + run: async () => { + delegate.setPermissionLevel(ChatPermissionLevel.AutoApprove); + if (this.element) { + this.renderLabel(this.element); + } + }, + } satisfies IActionWidgetDropdownAction, + ]; + if (isAutopilotEnabled()) { + actions.push({ + ...action, + id: 'chat.permissions.autopilot', + label: localize('permissions.autopilot', "Autopilot (Preview)"), + icon: ThemeIcon.fromId(Codicon.rocket.id), + checked: currentLevel === ChatPermissionLevel.Autopilot, + enabled: !policyRestricted, + tooltip: policyRestricted ? localize('permissions.autopilot.policyDisabled', "Disabled by enterprise policy") : '', + hover: { + content: policyRestricted + ? localize('permissions.autopilot.policyDescription', "Disabled by enterprise policy") + : localize('permissions.autopilot.description', "Auto-approve all tool calls and continue until the task is done"), + position: pickerOptions.hoverPosition + }, + run: async () => { + delegate.setPermissionLevel(ChatPermissionLevel.Autopilot); + if (this.element) { + this.renderLabel(this.element); + } + }, + } satisfies IActionWidgetDropdownAction); + } + return actions; + } + }; + + super(action, { + actionProvider, + reporter: { id: 'ChatPermissionPicker', name: 'ChatPermissionPicker', includeOptions: true }, + }, pickerOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); + } + + protected override renderLabel(element: HTMLElement): IDisposable | null { + this.setAriaLabelAttributes(element); + + const level = this.delegate.currentPermissionLevel.get(); + let icon: ThemeIcon; + let label: string; + switch (level) { + case ChatPermissionLevel.Autopilot: + icon = Codicon.rocket; + label = localize('permissions.autopilot.label', "Autopilot (Preview)"); + break; + case ChatPermissionLevel.AutoApprove: + icon = Codicon.warning; + label = localize('permissions.autoApprove.label', "Bypass Approvals"); + break; + default: + icon = Codicon.shield; + label = localize('permissions.default.label', "Default Approvals"); + break; + } + + const labelElements = []; + labelElements.push(...renderLabelWithIcons(`$(${icon.id})`)); + labelElements.push(dom.$('span.chat-input-picker-label', undefined, label)); + labelElements.push(...renderLabelWithIcons(`$(chevron-down)`)); + + dom.reset(element, ...labelElements); + element.classList.toggle('warning', level === ChatPermissionLevel.Autopilot); + element.classList.toggle('info', level === ChatPermissionLevel.AutoApprove); + return null; + } + + public refresh(): void { + if (this.element) { + this.renderLabel(this.element); + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index 897c927f25fd8..9ef9fb5eab66a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -213,12 +213,9 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { const icon = getAgentSessionProviderIcon(currentType ?? AgentSessionProviders.Local); const labelElements = []; - const collapsed = this.pickerOptions.hideChevrons.get(); labelElements.push(...renderLabelWithIcons(`$(${icon.id})`)); - if (!collapsed) { - labelElements.push(dom.$('span.chat-input-picker-label', undefined, label)); - labelElements.push(...renderLabelWithIcons(`$(chevron-down)`)); - } + labelElements.push(dom.$('span.chat-input-picker-label', undefined, label)); + labelElements.push(...renderLabelWithIcons(`$(chevron-down)`)); dom.reset(element, ...labelElements); diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 86c54b7bd96c3..dd200620d82b9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -822,11 +822,18 @@ have to be updated for changes to the rules above, or to support more deeply nes overflow: hidden; } -/* Context usage widget container - positioned in the bottom toolbar */ -.interactive-session .chat-input-toolbars .chat-context-usage-container { +/* Context usage widget container - positioned in the secondary toolbar below input */ +.interactive-session .chat-input-toolbars .chat-context-usage-container, +.interactive-session .chat-secondary-toolbar .chat-context-usage-container { display: flex; align-items: center; flex-shrink: 0; + margin-left: auto; + order: 1; +} + +/* When context usage is inside the toolbars (compact mode), keep the ordering */ +.interactive-session .chat-input-toolbars .chat-context-usage-container { order: 1; } @@ -1330,6 +1337,86 @@ have to be updated for changes to the rules above, or to support more deeply nes margin-top: 4px; } +/* Secondary toolbar below the input box */ +.interactive-session .chat-secondary-toolbar { + display: flex; + align-items: center; + gap: 6px; + padding: 0 4px 0 5px; +} + +.interactive-session .chat-secondary-toolbar:empty { + display: none; +} + +.interactive-session .chat-secondary-toolbar > .chat-secondary-input-toolbar { + overflow: hidden; + min-width: 0px; + color: var(--vscode-icon-foreground); + + .monaco-action-bar .action-item .codicon { + color: var(--vscode-icon-foreground); + } + + .chat-input-picker-item { + min-width: 0px; + overflow: hidden; + + .action-label { + min-width: 0px; + overflow: hidden; + position: relative; + + .chat-input-picker-label { + overflow: hidden; + text-overflow: ellipsis; + } + + span + .chat-input-picker-label { + margin-left: 2px; + } + + .codicon { + font-size: 12px; + } + } + + .codicon { + flex-shrink: 0; + } + } +} + +.interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label { + height: 16px; + padding: 3px 0px 3px 6px; + display: flex; + align-items: center; + color: var(--vscode-icon-foreground); +} + +.interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label.warning { + color: var(--vscode-problemsWarningIcon-foreground); +} + +.interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label.warning .codicon { + color: var(--vscode-problemsWarningIcon-foreground) !important; +} + +.interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label.info { + color: var(--vscode-problemsInfoIcon-foreground); +} + +.interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label.info .codicon { + color: var(--vscode-problemsInfoIcon-foreground) !important; +} + +.monaco-workbench .interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label .codicon-chevron-down { + font-size: 10px; + margin-left: 4px; + opacity: 0.75; +} + .interactive-session .chat-input-toolbars :not(.responsive.chat-input-toolbar) .actions-container:first-child { margin-right: auto; } @@ -1439,25 +1526,32 @@ have to be updated for changes to the rules above, or to support more deeply nes } } -/* When chevrons are hidden but label is still shown (e.g. model picker), use equal padding */ -.interactive-session .chat-input-toolbar .chat-input-picker-item .action-label.hide-chevrons:has(.chat-input-picker-label), -.interactive-session .chat-input-toolbar .chat-input-picker-item.hide-chevrons .action-label:has(.chat-input-picker-label), -.interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label.hide-chevrons:has(.chat-input-picker-label) { - padding: 3px 7px; -} /* Hide the tools button when the toolbar is in collapsed state */ .interactive-session .chat-input-toolbar:has(.hide-chevrons) .action-item:has(.codicon-settings) { display: none; } +/* Add context button icon sizing */ +.interactive-session .chat-input-toolbar .action-item:has(.codicon-add) .action-label { + display: flex; + align-items: center; + justify-content: center; +} + +.interactive-session .chat-input-toolbar .action-item:has(.codicon-add) .codicon-add { + font-size: 14px; +} + .monaco-workbench .interactive-session .chat-input-toolbar .chat-input-picker-item .action-label .codicon-chevron-down, .monaco-workbench .interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label .codicon-chevron-down { - font-size: 12px; - margin-left: 2px; + font-size: 10px; + margin-left: 4px; + opacity: 0.75; } -.interactive-session .chat-input-toolbars .monaco-action-bar .actions-container { +.interactive-session .chat-input-toolbars .monaco-action-bar .actions-container, +.interactive-session .chat-secondary-toolbar .monaco-action-bar .actions-container { display: flex; gap: 4px; } @@ -1669,7 +1763,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .interactive-input-part { margin: 0px 12px; - padding: 4px 0 12px 0px; + padding: 4px 0 4px 0px; display: flex; flex-direction: column; gap: 4px; diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts index b20f39c7cf678..e051fbcfb3f8e 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts @@ -92,6 +92,7 @@ export class ChatContextUsageWidget extends Disposable { readonly domNode: HTMLElement; private readonly progressIndicator: CircularProgressIndicator; + private readonly percentageLabel: HTMLElement; private readonly _isVisible = observableValue(this, false); get isVisible(): IObservable { return this._isVisible; } @@ -130,6 +131,9 @@ export class ChatContextUsageWidget extends Disposable { this.progressIndicator = new CircularProgressIndicator(); iconContainer.appendChild(this.progressIndicator.domNode); + // Percentage label (visible on hover/focus) + this.percentageLabel = this.domNode.appendChild($('.percentage-label')); + // Track context usage opened state this._contextUsageOpenedKey = ChatContextKeys.contextUsageHasBeenOpened.bindTo(this.contextKeyService); @@ -286,6 +290,11 @@ export class ChatContextUsageWidget extends Disposable { // Update pie chart progress this.progressIndicator.setProgress(percentage); + // Update percentage label and aria-label + const roundedPercentage = Math.round(percentage); + this.percentageLabel.textContent = `${roundedPercentage}%`; + this.domNode.setAttribute('aria-label', localize('contextUsagePercentageLabel', "Context window usage: {0}%", roundedPercentage)); + // Update color based on usage level this.domNode.classList.remove('warning', 'error'); if (percentage >= 90) { 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 905351e3adfc5..ab722bf9ad486 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -540,6 +540,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { supportsChangingModes: true, dndContainer: parent, inputEditorMinLines: this.workbenchEnvironmentService.isSessionsWindow ? 2 : undefined, + isSessionsWindow: this.workbenchEnvironmentService.isSessionsWindow, }, { listForeground: SIDE_BAR_FOREGROUND, diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css index c3abb1332f0de..e09c5e1aa2321 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css @@ -6,9 +6,7 @@ .chat-context-usage-widget { display: flex; align-items: center; - justify-content: center; - height: 22px; - width: 22px; + gap: 4px; flex-shrink: 0; cursor: pointer; padding: 3px; @@ -55,7 +53,7 @@ .chat-context-usage-widget .progress-arc { fill: none; - stroke: var(--vscode-descriptionForeground); + stroke: var(--vscode-icon-foreground); stroke-width: 4; stroke-linecap: round; transform: rotate(-90deg); @@ -70,3 +68,20 @@ .chat-context-usage-widget.error .progress-arc { stroke: var(--vscode-editorError-foreground); } + +.chat-context-usage-widget .percentage-label { + font-size: 11px; + line-height: 1; + color: var(--vscode-descriptionForeground); + white-space: nowrap; + max-width: 0; + opacity: 0; + overflow: hidden; + transition: max-width 0.1s ease-out, opacity 0.1s ease-out; +} + +.chat-context-usage-widget:hover .percentage-label, +.chat-context-usage-widget:focus .percentage-label { + max-width: 4em; + opacity: 1; +} diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index 926ba9d9f9754..cce18bceca5c9 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -9,7 +9,7 @@ import { IsWebContext } from '../../../../../platform/contextkey/common/contextk import { RemoteNameContext } from '../../../../common/contextkeys.js'; import { ViewContainerLocation } from '../../../../common/views.js'; import { ChatEntitlementContextKeys } from '../../../../services/chat/common/chatEntitlementService.js'; -import { ChatAgentLocation, ChatModeKind } from '../constants.js'; +import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../constants.js'; export namespace ChatContextKeys { export const responseVote = new RawContextKey('chatSessionResponseVote', '', { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); @@ -46,6 +46,7 @@ export namespace ChatContextKeys { export const multipleChatTips = new RawContextKey('multipleChatTips', false, { type: 'boolean', description: localize('multipleChatTips', "True when there are multiple chat tips available.") }); export const inChatTerminalToolOutput = new RawContextKey('inChatTerminalToolOutput', false, { type: 'boolean', description: localize('inChatTerminalToolOutput', "True when focus is in the chat terminal output region.") }); export const chatModeKind = new RawContextKey('chatAgentKind', ChatModeKind.Ask, { type: 'string', description: localize('agentKind', "The 'kind' of the current agent.") }); + export const chatPermissionLevel = new RawContextKey('chatPermissionLevel', ChatPermissionLevel.Default, { type: 'string', description: localize('chatPermissionLevel', "The current permission level for tool auto-approval.") }); export const chatModeName = new RawContextKey('chatModeName', '', { type: 'string', description: localize('chatModeName', "The name of the current chat mode (e.g. 'Plan' for custom modes).") }); export const chatModelId = new RawContextKey('chatModelId', '', { type: 'string', description: localize('chatModelId', "The short id of the currently selected chat model (for example 'gpt-4.1').") }); diff --git a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts index de4108ff5fba5..286257580e22b 100644 --- a/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts +++ b/src/vs/workbench/contrib/chat/common/aiCustomizationWorkspaceService.ts @@ -23,6 +23,7 @@ export const AICustomizationManagementSection = { Prompts: 'prompts', Hooks: 'hooks', McpServers: 'mcpServers', + Plugins: 'plugins', Models: 'models', } as const; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index 0d1bb0635de2f..a1e1d22d80c1a 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -53,7 +53,7 @@ import { ChatMessageRole, IChatMessage, ILanguageModelsService } from '../langua import { ILanguageModelToolsService } from '../tools/languageModelToolsService.js'; import { ChatSessionOperationLog } from '../model/chatSessionOperationLog.js'; import { IPromptsService } from '../promptSyntax/service/promptsService.js'; -import { ChatRequestHooks } from '../promptSyntax/hookSchema.js'; +import { ChatRequestHooks, mergeHooks } from '../promptSyntax/hookSchema.js'; const serializedChatKey = 'interactive.sessions'; @@ -959,6 +959,20 @@ export class ChatService extends Disposable implements IChatService { this.logService.warn('[ChatService] Failed to collect hooks:', error); } + // Merge hooks from the selected custom agent's frontmatter (if any) + const agentName = options?.modeInfo?.modeInstructions?.name; + if (agentName) { + try { + const agents = await this.promptsService.getCustomAgents(token, model.sessionResource); + const customAgent = agents.find(a => a.name === agentName); + if (customAgent?.hooks) { + collectedHooks = mergeHooks(collectedHooks, customAgent.hooks); + } + } catch (error) { + this.logService.warn('[ChatService] Failed to collect agent hooks:', error); + } + } + const stopWatch = new StopWatch(false); store.add(token.onCancellationRequested(() => { this.trace('sendRequest', `Request for session ${model.sessionResource} was cancelled`); @@ -1024,6 +1038,7 @@ export class ChatService extends Disposable implements IChatService { userSelectedModelId: options?.userSelectedModelId, userSelectedTools: options?.userSelectedTools?.get(), modeInstructions: options?.modeInfo?.modeInstructions, + permissionLevel: options?.modeInfo?.permissionLevel, editedFileEvents: request.editedFileEvents, hooks: collectedHooks, hasHooksEnabled: !!collectedHooks && Object.values(collectedHooks).some(arr => arr.length > 0), @@ -1180,6 +1195,7 @@ export class ChatService extends Disposable implements IChatService { shouldProcessPending = !rawResult.errorDetails && !token.isCancellationRequested; request.response?.complete(); + if (agentOrCommandFollowups) { agentOrCommandFollowups.then(followups => { model.setFollowups(request!, followups); diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 9ed80f0451e93..1cc89241a274f 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -54,6 +54,7 @@ export enum ChatConfiguration { ExplainChangesEnabled = 'chat.editing.explainChanges.enabled', GrowthNotificationEnabled = 'chat.growthNotification.enabled', ChatCustomizationMenuEnabled = 'chat.customizationsMenu.enabled', + AutopilotEnabled = 'chat.autopilot.enabled', } /** @@ -76,6 +77,26 @@ export function validateChatMode(mode: unknown): ChatModeKind | undefined { } } +/** + * The permission level controlling tool auto-approval behavior. + */ +export enum ChatPermissionLevel { + /** Use existing auto-approve settings */ + Default = 'default', + /** Auto-approve all tool calls, auto-retry on error */ + AutoApprove = 'autoApprove', + /** Everything AutoApprove does plus an internal stop hook that continues until the task is done */ + Autopilot = 'autopilot' +} + +/** + * Returns true if the permission level enables auto-approval of all tool calls. + * Both {@link ChatPermissionLevel.AutoApprove} and {@link ChatPermissionLevel.Autopilot} enable auto-approval. + */ +export function isAutoApproveLevel(level: ChatPermissionLevel | undefined): boolean { + return level === ChatPermissionLevel.AutoApprove || level === ChatPermissionLevel.Autopilot; +} + export function isChatMode(mode: unknown): mode is ChatModeKind { return !!validateChatMode(mode); } diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 3fb3a88c82fb5..98098bb937681 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -30,7 +30,7 @@ import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCo import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isImplicitVariableEntry, isStringImplicitContextValue, isStringVariableEntry } from '../attachments/chatVariableEntries.js'; import { migrateLegacyTerminalToolSpecificData } from '../chat.js'; import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatRequestQueueKind, ChatResponseClearToPreviousToolInvocationReason, ElicitationState, IChatAgentMarkdownContentWithVulnerability, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatDisabledClaudeHooksPart, IChatEditingSessionAction, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExternalToolInvocationUpdate, IChatExtensionsContent, IChatFollowup, IChatHookPart, IChatLocationData, IChatMarkdownContent, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatModelReference, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionTiming, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsage, IChatUsedContext, IChatWarningMessage, IChatWorkspaceEdit, ResponseModelState, isIUsedContext } from '../chatService/chatService.js'; -import { ChatAgentLocation, ChatModeKind } from '../constants.js'; +import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../constants.js'; import { ChatToolInvocation } from './chatProgressTypes/chatToolInvocation.js'; import { ToolDataSource, IToolData } from '../tools/languageModelToolsService.js'; import { IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../editing/chatEditingService.js'; @@ -314,6 +314,7 @@ export interface IChatRequestModeInfo { modeInstructions: IChatRequestModeInstructions | undefined; modeId: 'ask' | 'agent' | 'edit' | 'custom' | 'applyCodeBlock' | undefined; applyCodeBlockSuggestionId: EditSuggestionId | undefined; + permissionLevel?: ChatPermissionLevel; } export interface IChatRequestModeInstructions { diff --git a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts index 817ebbb18b820..e8f4fbe4a03bc 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts @@ -24,7 +24,7 @@ import { IChatAgentEditedFileEvent, IChatProgressHistoryResponseContent, IChatRe import { ChatRequestHooks } from '../promptSyntax/hookSchema.js'; import { IRawChatCommandContribution } from './chatParticipantContribTypes.js'; import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from '../chatService/chatService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel } from '../constants.js'; import { ILanguageModelsService } from '../languageModels.js'; //#region agent service, commands etc @@ -158,6 +158,12 @@ export interface IChatAgentRequest { * Whether any hooks are enabled for this request. */ hasHooksEnabled?: boolean; + /** + * The permission level for tool auto-approval in this request. + * - `'autoApprove'`: Auto-approve all tool calls and retry on errors. + * - `'autopilot'`: Everything autoApprove does plus continues until the task is done. + */ + permissionLevel?: ChatPermissionLevel; /** * Unique ID for the subagent invocation, used to group tool calls from the same subagent run together. */ diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts index 6776c6a2e282a..9311488d61c7e 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookClaudeCompat.ts @@ -4,10 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from '../../../../../base/common/uri.js'; -import { toHookType, resolveHookCommand, IHookCommand } from './hookSchema.js'; +import { toHookType, IHookCommand, extractHookCommandsFromItem } from './hookSchema.js'; import { HOOKS_BY_TARGET, HookType } from './hookTypes.js'; import { Target } from './promptTypes.js'; +export { extractHookCommandsFromItem }; + /** * Cached inverse mapping from HookType to Claude hook type name. * Lazily computed on first access. @@ -132,60 +134,4 @@ export function parseClaudeHooks( return { hooks: result, disabledAllHooks: false }; } -/** - * Helper to extract hook commands from an item that could be: - * 1. A direct command object: { type: 'command', command: '...' } - * 2. A nested structure with matcher (Claude style): { matcher: '...', hooks: [{ type: 'command', command: '...' }] } - * - * This allows Copilot format to handle Claude-style entries if pasted. - * Also handles Claude's leniency where 'type' field can be omitted. - */ -export function extractHookCommandsFromItem( - item: unknown, - workspaceRootUri: URI | undefined, - userHome: string -): IHookCommand[] { - if (!item || typeof item !== 'object') { - return []; - } - - const itemObj = item as Record; - const commands: IHookCommand[] = []; - - // Check for nested hooks with matcher (Claude style): { matcher: "...", hooks: [...] } - const nestedHooks = itemObj.hooks; - if (nestedHooks !== undefined && Array.isArray(nestedHooks)) { - for (const nestedHook of nestedHooks) { - if (!nestedHook || typeof nestedHook !== 'object') { - continue; - } - const normalized = normalizeForResolve(nestedHook as Record); - const resolved = resolveHookCommand(normalized, workspaceRootUri, userHome); - if (resolved) { - commands.push(resolved); - } - } - } else { - // Direct command object - const normalized = normalizeForResolve(itemObj); - const resolved = resolveHookCommand(normalized, workspaceRootUri, userHome); - if (resolved) { - commands.push(resolved); - } - } - - return commands; -} -/** - * Normalizes a hook command object for resolving. - * Claude format allows omitting the 'type' field, treating it as 'command'. - * This ensures compatibility when Claude-style hooks are pasted into Copilot format. - */ -function normalizeForResolve(raw: Record): Record { - // If type is missing or already 'command', ensure it's set to 'command' - if (raw.type === undefined || raw.type === 'command') { - return { ...raw, type: 'command' }; - } - return raw; -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts index 0d69dce53bff9..8025b3ea67598 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts @@ -12,6 +12,7 @@ import { untildify } from '../../../../../base/common/labels.js'; import { OperatingSystem } from '../../../../../base/common/platform.js'; import { HookType, HOOKS_BY_TARGET, HOOK_METADATA } from './hookTypes.js'; import { Target } from './promptTypes.js'; +import { IValue, IMapValue } from './promptFileParser.js'; /** * A single hook command configuration. @@ -46,6 +47,43 @@ export type ChatRequestHooks = { readonly [K in HookType]?: readonly IHookCommand[]; }; +/** + * Merges two sets of hooks by concatenating the command arrays for each hook type. + * Additional hooks are appended after the base hooks. + */ +export function mergeHooks(base: ChatRequestHooks | undefined, additional: ChatRequestHooks): ChatRequestHooks { + if (!base) { + return additional; + } + + const result: Partial> = { ...base }; + for (const hookType of Object.values(HookType)) { + const baseArr = base[hookType]; + const additionalArr = additional[hookType]; + if (additionalArr && additionalArr.length > 0) { + result[hookType] = baseArr ? [...baseArr, ...additionalArr] : additionalArr; + } + } + return result as ChatRequestHooks; +} + +/** + * Descriptions for hook command fields, used by both the JSON schema and the hover provider. + */ +export const HOOK_COMMAND_FIELD_DESCRIPTIONS: Record = { + type: nls.localize('hook.type', 'Must be "command".'), + command: nls.localize('hook.command', 'The command to execute. This is the default cross-platform command.'), + windows: nls.localize('hook.windows', 'Windows-specific command. If specified and running on Windows, this overrides the "command" field.'), + linux: nls.localize('hook.linux', 'Linux-specific command. If specified and running on Linux, this overrides the "command" field.'), + osx: nls.localize('hook.osx', 'macOS-specific command. If specified and running on macOS, this overrides the "command" field.'), + bash: nls.localize('hook.bash', 'Bash command for Linux and macOS.'), + powershell: nls.localize('hook.powershell', 'PowerShell command for Windows.'), + cwd: nls.localize('hook.cwd', 'Working directory for the script (relative to repository root).'), + env: nls.localize('hook.env', 'Additional environment variables that are merged with the existing environment.'), + timeout: nls.localize('hook.timeout', 'Maximum execution time in seconds (default: 30).'), + timeoutSec: nls.localize('hook.timeoutSec', 'Maximum execution time in seconds (default: 10).'), +}; + /** * JSON Schema for GitHub Copilot hook configuration files. * Hooks enable executing custom shell commands at strategic points in an agent's workflow. @@ -67,37 +105,37 @@ const vscodeHookCommandSchema: IJSONSchema = { type: { type: 'string', enum: ['command'], - description: nls.localize('hook.type', 'Must be "command".') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.type }, command: { type: 'string', - description: nls.localize('hook.command', 'The command to execute. This is the default cross-platform command.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.command }, windows: { type: 'string', - description: nls.localize('hook.windows', 'Windows-specific command. If specified and running on Windows, this overrides the "command" field.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.windows }, linux: { type: 'string', - description: nls.localize('hook.linux', 'Linux-specific command. If specified and running on Linux, this overrides the "command" field.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.linux }, osx: { type: 'string', - description: nls.localize('hook.osx', 'macOS-specific command. If specified and running on macOS, this overrides the "command" field.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.osx }, cwd: { type: 'string', - description: nls.localize('hook.cwd', 'Working directory for the script (relative to repository root).') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.cwd }, env: { type: 'object', additionalProperties: { type: 'string' }, - description: nls.localize('hook.env', 'Additional environment variables that are merged with the existing environment.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.env }, timeout: { type: 'number', default: 30, - description: nls.localize('hook.timeout', 'Maximum execution time in seconds (default: 30).') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.timeout } } }; @@ -142,29 +180,29 @@ const copilotCliHookCommandSchema: IJSONSchema = { type: { type: 'string', enum: ['command'], - description: nls.localize('hook.type', 'Must be "command".') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.type }, bash: { type: 'string', - description: nls.localize('hook.bash', 'Bash command for Linux and macOS.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.bash }, powershell: { type: 'string', - description: nls.localize('hook.powershell', 'PowerShell command for Windows.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.powershell }, cwd: { type: 'string', - description: nls.localize('hook.cwd', 'Working directory for the script (relative to repository root).') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.cwd }, env: { type: 'object', additionalProperties: { type: 'string' }, - description: nls.localize('hook.env', 'Additional environment variables that are merged with the existing environment.') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.env }, timeoutSec: { type: 'number', default: 10, - description: nls.localize('hook.timeoutSec', 'Maximum execution time in seconds (default: 10).') + description: HOOK_COMMAND_FIELD_DESCRIPTIONS.timeoutSec } } }; @@ -444,3 +482,155 @@ export function resolveHookCommand(raw: Record, workspaceRootUr ...(normalized.timeout !== undefined && { timeout: normalized.timeout }), }; } + +/** + * Helper to extract hook commands from an item that could be: + * 1. A direct command object: { type: 'command', command: '...' } + * 2. A nested structure with matcher (Claude style): { matcher: '...', hooks: [{ type: 'command', command: '...' }] } + * + * This allows Copilot format to handle Claude-style entries if pasted. + * Also handles Claude's leniency where 'type' field can be omitted. + */ +export function extractHookCommandsFromItem( + item: unknown, + workspaceRootUri: URI | undefined, + userHome: string +): IHookCommand[] { + if (!item || typeof item !== 'object') { + return []; + } + + const itemObj = item as Record; + const commands: IHookCommand[] = []; + + // Check for nested hooks with matcher (Claude style): { matcher: "...", hooks: [...] } + const nestedHooks = itemObj.hooks; + if (nestedHooks !== undefined && Array.isArray(nestedHooks)) { + for (const nestedHook of nestedHooks) { + if (!nestedHook || typeof nestedHook !== 'object') { + continue; + } + const normalized = normalizeForResolve(nestedHook as Record); + const resolved = resolveHookCommand(normalized, workspaceRootUri, userHome); + if (resolved) { + commands.push(resolved); + } + } + } else { + // Direct command object + const normalized = normalizeForResolve(itemObj); + const resolved = resolveHookCommand(normalized, workspaceRootUri, userHome); + if (resolved) { + commands.push(resolved); + } + } + + return commands; +} + +/** + * Normalizes a hook command object for resolving. + * Claude format allows omitting the 'type' field, treating it as 'command'. + * This ensures compatibility when Claude-style hooks are pasted into Copilot format. + */ +function normalizeForResolve(raw: Record): Record { + // If type is missing or already 'command', ensure it's set to 'command' + if (raw.type === undefined || raw.type === 'command') { + return { ...raw, type: 'command' }; + } + return raw; +} + +/** + * Converts an {@link IValue} YAML AST node into a plain JavaScript value + * (string, array, or object) suitable for passing to hook parsing helpers. + */ +function yamlValueToPlain(value: IValue): unknown { + switch (value.type) { + case 'scalar': + return value.value; + case 'sequence': + return value.items.map(yamlValueToPlain); + case 'map': { + const obj: Record = {}; + for (const prop of value.properties) { + obj[prop.key.value] = yamlValueToPlain(prop.value); + } + return obj; + } + } +} + +/** + * Parses hooks from a subagent's YAML frontmatter `hooks` attribute. + * + * Supports two formats for hook entries: + * + * 1. **Direct command** (our format, without matcher): + * ```yaml + * hooks: + * PreToolUse: + * - type: command + * command: "./scripts/validate.sh" + * ``` + * + * 2. **Nested with matcher** (Claude Code format): + * ```yaml + * hooks: + * PreToolUse: + * - matcher: "Bash" + * hooks: + * - type: command + * command: "./scripts/validate.sh" + * ``` + * + * @param hooksMap The raw YAML map value from the `hooks` frontmatter attribute. + * @param workspaceRootUri Workspace root for resolving relative `cwd` paths. + * @param userHome User home directory path for tilde expansion. + * @param target The agent's target, used to resolve hook type names correctly. + * @returns Resolved hooks organized by hook type, ready for use in {@link ChatRequestHooks}. + */ +export function parseSubagentHooksFromYaml( + hooksMap: IMapValue, + workspaceRootUri: URI | undefined, + userHome: string, + target: Target = Target.Undefined, +): ChatRequestHooks { + const result: Record = {}; + const targetHookMap = HOOKS_BY_TARGET[target] ?? HOOKS_BY_TARGET[Target.Undefined]; + + for (const prop of hooksMap.properties) { + const hookTypeName = prop.key.value; + + // Resolve hook type name using the target's own map first, then fall back to canonical names + const hookType = targetHookMap[hookTypeName] ?? toHookType(hookTypeName); + if (!hookType) { + continue; + } + + // The value must be a sequence (array of hook entries) + if (prop.value.type !== 'sequence') { + continue; + } + + const commands: IHookCommand[] = []; + + for (const item of prop.value.items) { + // Convert the YAML AST node to a plain object so the existing + // extractHookCommandsFromItem helper can handle both direct + // commands and nested matcher structures. + const plainItem = yamlValueToPlain(item); + const extracted = extractHookCommandsFromItem(plainItem, workspaceRootUri, userHome); + commands.push(...extracted); + } + + if (commands.length > 0) { + if (!result[hookType]) { + result[hookType] = []; + } + result[hookType].push(...commands); + } + } + + return result as ChatRequestHooks; +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptFileAttributes.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptFileAttributes.ts index 2e99a13df8223..a57216523c997 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptFileAttributes.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptFileAttributes.ts @@ -167,6 +167,10 @@ export const customAgentAttributes: Record = { type: 'map', description: localize('promptHeader.agent.github', 'GitHub-specific configuration for the agent, such as token permissions.'), }, + [PromptHeaderAttributes.hooks]: { + type: 'map', + description: localize('promptHeader.agent.hooks', 'Lifecycle hooks scoped to this agent. Define hooks that run only while this agent is active.'), + }, }; // Attribute metadata for skill files (`SKILL.md`). diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts index 14009b90d4498..4884aed9fa863 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -15,10 +15,12 @@ import { IChatModeService } from '../../chatModes.js'; import { getPromptsTypeForLanguageId, PromptsType, Target } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; import { Iterable } from '../../../../../../base/common/iterator.js'; -import { ISequenceValue, IValue, parseCommaSeparatedList, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; +import { IMapValue, ISequenceValue, IValue, IHeaderAttribute, parseCommaSeparatedList, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; import { getAttributeDefinition, getTarget, getValidAttributeNames, knownClaudeTools, knownGithubCopilotTools, IValueEntry, ClaudeHeaderAttributes, } from './promptFileAttributes.js'; import { localize } from '../../../../../../nls.js'; import { formatArrayValue, getQuotePreference } from '../utils/promptEditHelper.js'; +import { HOOKS_BY_TARGET, HOOK_METADATA } from '../hookTypes.js'; +import { HOOK_COMMAND_FIELD_DESCRIPTIONS } from '../hookSchema.js'; export class PromptHeaderAutocompletion implements CompletionItemProvider { /** @@ -91,6 +93,33 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { const colonPosition = colonIndex !== -1 ? new Position(position.lineNumber, colonIndex + 1) : undefined; if (!colonPosition || position.isBeforeOrEqual(colonPosition)) { + // Check if the position is inside a multi-line attribute (e.g., hooks map). + // In that case, provide value completions for that attribute instead of attribute name completions. + let containingAttribute = header.attributes.find(({ range }) => + range.startLineNumber < position.lineNumber && position.lineNumber <= range.endLineNumber); + if (!containingAttribute) { + // Handle trailing empty lines after a map-valued attribute: + // The YAML parser's range ends at the last parsed child, but logically + // an empty line before the next attribute still belongs to the map. + for (let i = header.attributes.length - 1; i >= 0; i--) { + const attr = header.attributes[i]; + if (attr.range.endLineNumber < position.lineNumber && attr.value.type === 'map') { + const nextAttr = header.attributes[i + 1]; + const nextStartLine = nextAttr ? nextAttr.range.startLineNumber : headerRange.endLineNumber; + if (position.lineNumber < nextStartLine) { + containingAttribute = attr; + } + break; + } + } + } + if (containingAttribute) { + const attrLineText = model.getLineContent(containingAttribute.range.startLineNumber); + const attrColonIndex = attrLineText.indexOf(':'); + if (attrColonIndex !== -1) { + return this.provideValueCompletions(model, position, header, new Position(containingAttribute.range.startLineNumber, attrColonIndex + 1), promptType, containingAttribute); + } + } return this.provideAttributeNameCompletions(model, position, header, colonPosition, promptType); } else if (colonPosition && colonPosition.isBefore(position)) { return this.provideValueCompletions(model, position, header, colonPosition, promptType); @@ -116,6 +145,11 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { if (colonPosition) { return key; } + // For map-valued attributes, insert a snippet with the nested structure + if (key === PromptHeaderAttributes.hooks && promptType === PromptsType.agent && target !== Target.Claude) { + const hookNames = Object.keys(HOOKS_BY_TARGET[target] ?? HOOKS_BY_TARGET[Target.Undefined]); + return `${key}:\n \${1|${hookNames.join(',')}|}:\n - type: command\n command: "$2"`; + } const valueSuggestions = this.getValueSuggestions(promptType, key, target); if (valueSuggestions.length > 0) { return `${key}: \${0:${valueSuggestions[0].name}}`; @@ -146,10 +180,11 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { header: PromptHeader, colonPosition: Position, promptType: PromptsType, + preFoundAttribute?: IHeaderAttribute, ): Promise { const suggestions: CompletionItem[] = []; const posLineNumber = position.lineNumber; - const attribute = header.attributes.find(({ range }) => range.startLineNumber <= posLineNumber && posLineNumber <= range.endLineNumber); + const attribute = preFoundAttribute ?? header.attributes.find(({ range }) => range.startLineNumber <= posLineNumber && posLineNumber <= range.endLineNumber); if (!attribute) { return undefined; } @@ -200,6 +235,18 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { }); } } + if (attribute.key === PromptHeaderAttributes.hooks) { + if (attribute.value.type === 'map') { + // Inside the hooks map — suggest hook event type names as sub-keys + return this.provideHookEventCompletions(model, position, attribute.value, target); + } + // When hooks value is not yet a map (e.g., user is mid-edit on a nested line), + // still provide hook event completions with no existing keys. + if (position.lineNumber !== attribute.range.startLineNumber) { + const emptyMap: IMapValue = { type: 'map', properties: [], range: attribute.value.range }; + return this.provideHookEventCompletions(model, position, emptyMap, target); + } + } const lineContent = model.getLineContent(attribute.range.startLineNumber); const whilespaceAfterColon = (lineContent.substring(colonPosition.column).match(/^\s*/)?.[0].length) ?? 0; const entries = this.getValueSuggestions(promptType, attribute.key, target); @@ -229,9 +276,290 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { }; suggestions.push(item); } + if (attribute.key === PromptHeaderAttributes.hooks && promptType === PromptsType.agent) { + const hookSnippet = [ + '', + ' ${1|' + Object.keys(HOOKS_BY_TARGET[target] ?? HOOKS_BY_TARGET[Target.Undefined]).join(',') + '|}:', + ' - type: command', + ' command: "$2"' + ].join('\n'); + const item: CompletionItem = { + label: localize('promptHeaderAutocompletion.newHook', "New Hook"), + kind: CompletionItemKind.Snippet, + insertText: whilespaceAfterColon === 0 ? ` ${hookSnippet}` : hookSnippet, + insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, + range: new Range(position.lineNumber, colonPosition.column + whilespaceAfterColon + 1, position.lineNumber, model.getLineMaxColumn(position.lineNumber)), + }; + suggestions.push(item); + } return { suggestions }; } + /** + * Provides completions inside the `hooks:` map. + * Determines what to suggest based on nesting depth: + * - At hook event level: suggest event names (SessionStart, PreToolUse, etc.) + * - Inside a command object: suggest command fields (type, command, timeout, etc.) + */ + private provideHookEventCompletions( + model: ITextModel, + position: Position, + hooksMap: IMapValue, + target: Target, + ): CompletionList | undefined { + // Check if the cursor is on the value side of an existing hook event key (e.g., "SessionEnd:|") + // In that case, offer a command entry snippet instead of event name completions. + const hookEventOnLine = hooksMap.properties.find(p => p.key.range.startLineNumber === position.lineNumber); + if (hookEventOnLine) { + const lineText = model.getLineContent(position.lineNumber); + const colonIdx = lineText.indexOf(':'); + if (colonIdx !== -1 && position.column > colonIdx + 1) { + const whilespaceAfterColon = (lineText.substring(colonIdx + 1).match(/^\s*/)?.[0].length) ?? 0; + const commandSnippet = [ + '', + ' - type: command', + ' command: "$1"', + ].join('\n'); + return { + suggestions: [{ + label: localize('promptHeaderAutocompletion.newCommand', "New Command"), + documentation: localize('promptHeaderAutocompletion.newCommand.description', "Add a new command entry to this hook."), + kind: CompletionItemKind.Snippet, + insertText: whilespaceAfterColon === 0 ? ` ${commandSnippet}` : commandSnippet, + insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, + range: new Range(position.lineNumber, colonIdx + 1 + whilespaceAfterColon + 1, position.lineNumber, model.getLineMaxColumn(position.lineNumber)), + }] + }; + } + } + + // Try to provide command field completions if cursor is inside a command object + const commandFieldCompletions = this.provideHookCommandFieldCompletions(model, position, hooksMap, target); + if (commandFieldCompletions) { + return commandFieldCompletions; + } + + // Otherwise provide hook event name completions + const suggestions: CompletionItem[] = []; + const hooksByTarget = HOOKS_BY_TARGET[target] ?? HOOKS_BY_TARGET[Target.Undefined]; + + const lineText = model.getLineContent(position.lineNumber); + const firstNonWhitespace = lineText.search(/\S/); + const isEmptyLine = firstNonWhitespace === -1; + // Start the range after leading whitespace so VS Code's completion + // filtering matches the hook name prefix the user has typed. + const rangeStartColumn = isEmptyLine ? position.column : firstNonWhitespace + 1; + + // Exclude hook keys on the current line so the user sees all options while editing a key + const existingKeys = new Set( + hooksMap.properties + .filter(p => p.key.range.startLineNumber !== position.lineNumber) + .map(p => p.key.value) + ); + + // Supplement with text-based scanning: when incomplete YAML causes the + // parser to drop subsequent keys, scan the model for lines that look + // like hook event entries (e.g., " UserPromptSubmit:") at the expected + // indentation. + const expectedIndent = hooksMap.properties.length > 0 + ? hooksMap.properties[0].key.range.startColumn - 1 + : -1; + if (expectedIndent >= 0) { + const scanEnd = model.getLineCount(); + for (let lineNum = hooksMap.range.endLineNumber + 1; lineNum <= scanEnd; lineNum++) { + if (lineNum === position.lineNumber) { + continue; + } + const lt = model.getLineContent(lineNum); + const lineIndent = lt.search(/\S/); + if (lineIndent === -1) { + continue; + } + if (lineIndent < expectedIndent) { + break; // Left the hooks map scope + } + if (lineIndent === expectedIndent) { + const match = lt.match(/^\s+(\S+)\s*:/); + if (match) { + existingKeys.add(match[1]); + } + } + } + } + + // Check whether the current line already has a colon (editing an existing key) + const lineHasColon = lineText.indexOf(':') !== -1; + + for (const [hookName, hookType] of Object.entries(hooksByTarget)) { + if (existingKeys.has(hookName)) { + continue; + } + const meta = HOOK_METADATA[hookType]; + let insertText: string; + if (isEmptyLine) { + // On empty lines, insert a full hook snippet with command placeholder + insertText = [ + `${hookName}:`, + ` - type: command`, + ` command: "$1"`, + ].join('\n'); + } else if (lineHasColon) { + // On existing key lines, only replace the key name to preserve nested content + insertText = `${hookName}:`; + } else { + // Typing a new event name — omit the colon so the user can + // trigger the next completion (e.g., New Command snippet) by typing ':' + insertText = hookName; + } + suggestions.push({ + label: hookName, + documentation: meta?.description, + kind: CompletionItemKind.Property, + insertText, + insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, + range: new Range(position.lineNumber, rangeStartColumn, position.lineNumber, model.getLineMaxColumn(position.lineNumber)), + }); + } + + return { suggestions }; + } + + /** + * Provides completions for hook command fields (type, command, windows, etc.) + * when the cursor is inside a command object within the hooks map. + * Detects nesting by checking if the position falls within a sequence item + * of a hook event's value. + */ + private provideHookCommandFieldCompletions( + model: ITextModel, + position: Position, + hooksMap: IMapValue, + target: Target, + ): CompletionList | undefined { + // Find which hook event's command list the cursor is in + const containingCommandMap = this.findContainingCommandMap(model, position, hooksMap); + if (!containingCommandMap) { + return undefined; + } + + const isCopilotCli = target === Target.GitHubCopilot; + const validFields = isCopilotCli + ? ['type', 'bash', 'powershell', 'cwd', 'env', 'timeoutSec'] + : ['type', 'command', 'windows', 'linux', 'osx', 'bash', 'powershell', 'cwd', 'env', 'timeout']; + + const existingFields = new Set( + containingCommandMap.properties + .filter(p => p.key.range.startLineNumber !== position.lineNumber) + .map(p => p.key.value) + ); + + const lineText = model.getLineContent(position.lineNumber); + const firstNonWhitespace = lineText.search(/\S/); + const isEmptyLine = firstNonWhitespace === -1; + // Skip past the YAML sequence indicator `- ` so the range starts at the + // actual field name; otherwise VS Code's completion filter would see the + // `- ` prefix and reject valid field names. + const dashPrefixMatch = lineText.match(/^(\s*-\s+)/); + const fieldStart = dashPrefixMatch ? dashPrefixMatch[1].length : firstNonWhitespace; + const rangeStartColumn = isEmptyLine ? position.column : fieldStart + 1; + const colonIndex = lineText.indexOf(':'); + + const suggestions: CompletionItem[] = []; + for (const fieldName of validFields) { + if (existingFields.has(fieldName)) { + continue; + } + const desc = HOOK_COMMAND_FIELD_DESCRIPTIONS[fieldName]; + const insertText = colonIndex !== -1 ? fieldName : `${fieldName}: $0`; + suggestions.push({ + label: fieldName, + documentation: desc, + kind: CompletionItemKind.Property, + insertText, + insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, + range: new Range(position.lineNumber, rangeStartColumn, position.lineNumber, colonIndex !== -1 ? colonIndex + 1 : model.getLineMaxColumn(position.lineNumber)), + }); + } + + return suggestions.length > 0 ? { suggestions } : undefined; + } + + /** + * Walks the hooks map AST to find the command map object containing the position. + * Handles both direct command objects and nested matcher format. + * Also handles trailing lines after the last parsed property of a command map. + */ + private findContainingCommandMap(model: ITextModel, position: Position, hooksMap: IMapValue): IMapValue | undefined { + for (let i = 0; i < hooksMap.properties.length; i++) { + const prop = hooksMap.properties[i]; + if (prop.value.type !== 'sequence') { + continue; + } + // Check if cursor is within the sequence's range, or on a trailing line after it + const seqRange = prop.value.range; + const nextProp = hooksMap.properties[i + 1]; + const isInSeq = seqRange.containsPosition(position); + const isTrailingSeq = !isInSeq + && seqRange.endLineNumber < position.lineNumber + && (!nextProp || nextProp.key.range.startLineNumber > position.lineNumber); + + if (isInSeq || isTrailingSeq) { + // For trailing lines, verify the cursor is indented deeper than + // the hook event key — otherwise it belongs to the parent map. + if (isTrailingSeq) { + const lineText = model.getLineContent(position.lineNumber); + const firstNonWs = lineText.search(/\S/); + const effectiveIndent = firstNonWs === -1 ? position.column - 1 : firstNonWs; + const hookKeyIndent = prop.key.range.startColumn - 1; + if (effectiveIndent <= hookKeyIndent) { + continue; + } + } + const result = this.findCommandMapInSequence(position, prop.value); + if (result) { + return result; + } + } + } + return undefined; + } + + private findCommandMapInSequence(position: Position, sequence: ISequenceValue): IMapValue | undefined { + for (let i = 0; i < sequence.items.length; i++) { + const item = sequence.items[i]; + if (item.type !== 'map') { + // Handle partial typing: a scalar on the cursor line means the user + // is starting to type a command entry (e.g., "- t"). + if (item.type === 'scalar' && item.range.startLineNumber === position.lineNumber) { + return { type: 'map', properties: [], range: item.range }; + } + continue; + } + + // Check if position is within or just after this map item's parsed range. + // The parser's range may not include a trailing line being typed. + const isInRange = item.range.containsPosition(position); + const isTrailing = !isInRange + && item.range.endLineNumber < position.lineNumber + && (i + 1 >= sequence.items.length || sequence.items[i + 1].range.startLineNumber > position.lineNumber); + + if (!isInRange && !isTrailing) { + continue; + } + + // Check for nested matcher format: { hooks: [...] } + const nestedHooks = item.properties.find(p => p.key.value === 'hooks'); + if (nestedHooks?.value.type === 'sequence') { + const result = this.findCommandMapInSequence(position, nestedHooks.value); + if (result) { + return result; + } + } + return item; + } + return undefined; + } + private getValueSuggestions(promptType: PromptsType, attribute: string, target: Target): readonly IValueEntry[] { const attributeDesc = getAttributeDefinition(attribute, promptType, target); if (attributeDesc?.enums) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts index 273ceef3be047..00065d1b40c3a 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts @@ -15,8 +15,10 @@ import { ILanguageModelToolsService, isToolSet, IToolSet } from '../../tools/lan import { IChatModeService, isBuiltinChatMode } from '../../chatModes.js'; import { getPromptsTypeForLanguageId, PromptsType, Target } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; -import { IHeaderAttribute, parseCommaSeparatedList, PromptBody, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; +import { IHeaderAttribute, ISequenceValue, parseCommaSeparatedList, PromptBody, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; import { ClaudeHeaderAttributes, getAttributeDefinition, getTarget, isVSCodeOrDefaultTarget, knownClaudeModels, knownClaudeTools } from './promptFileAttributes.js'; +import { HOOKS_BY_TARGET, HOOK_METADATA } from '../hookTypes.js'; +import { HOOK_COMMAND_FIELD_DESCRIPTIONS } from '../hookSchema.js'; export class PromptHoverProvider implements HoverProvider { /** @@ -86,6 +88,8 @@ export class PromptHoverProvider implements HoverProvider { return this.getAgentHover(attribute, position, description); case PromptHeaderAttributes.handOffs: return this.getHandsOffHover(attribute, position, target); + case PromptHeaderAttributes.hooks: + return this.getHooksHover(attribute, position, description, target); case PromptHeaderAttributes.infer: return this.createHover(description + '\n\n' + localize('promptHeader.attribute.infer.hover', 'Deprecated: Use `user-invocable` and `disable-model-invocation` instead.'), attribute.range); default: @@ -232,6 +236,62 @@ export class PromptHoverProvider implements HoverProvider { return this.createHover(lines.join('\n'), agentAttribute.range); } + private getHooksHover(attribute: IHeaderAttribute, position: Position, baseMessage: string, target: Target): Hover | undefined { + const value = attribute.value; + if (value.type === 'map') { + const hooksByTarget = HOOKS_BY_TARGET[target] ?? HOOKS_BY_TARGET[Target.Undefined]; + for (const prop of value.properties) { + // Hover on a hook event name key (e.g., SessionStart, PreToolUse) + if (prop.key.range.containsPosition(position)) { + const hookType = hooksByTarget[prop.key.value]; + if (hookType) { + const meta = HOOK_METADATA[hookType]; + return this.createHover(`**${meta.label}**\n\n${meta.description}`, prop.key.range); + } + } + // Hover inside hook command entries + if (prop.value.type === 'sequence') { + const hover = this.getHookCommandItemHover(prop.value, position); + if (hover) { + return hover; + } + } + } + } + return this.createHover(baseMessage, attribute.range); + } + + /** + * Recursively searches hook command items for hover information. + * Handles both direct command objects and nested matcher format + * (e.g., `{ matcher: "...", hooks: [{ type: command, ... }] }`). + */ + private getHookCommandItemHover(sequence: ISequenceValue, position: Position): Hover | undefined { + for (const item of sequence.items) { + if (item.type !== 'map' || !item.range.containsPosition(position)) { + continue; + } + // Check for nested matcher format: { hooks: [...] } + const nestedHooks = item.properties.find(p => p.key.value === 'hooks'); + if (nestedHooks && nestedHooks.value.type === 'sequence') { + const hover = this.getHookCommandItemHover(nestedHooks.value, position); + if (hover) { + return hover; + } + } + // Check fields of the command object itself + for (const field of item.properties) { + if (field.key.range.containsPosition(position) || field.value.range.containsPosition(position)) { + const desc = HOOK_COMMAND_FIELD_DESCRIPTIONS[field.key.value]; + if (desc) { + return this.createHover(desc, field.key.range); + } + } + } + } + return undefined; + } + private getHandsOffHover(attribute: IHeaderAttribute, position: Position, target: Target): Hover | undefined { const handoffsBaseMessage = getAttributeDefinition(PromptHeaderAttributes.handOffs, PromptsType.agent, target)?.description!; if (!isVSCodeOrDefaultTarget(target)) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index 569e024d3c866..d6e71491d019d 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -28,6 +28,7 @@ import { Lazy } from '../../../../../../base/common/lazy.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { dirname } from '../../../../../../base/common/resources.js'; import { URI } from '../../../../../../base/common/uri.js'; +import { HOOKS_BY_TARGET } from '../hookTypes.js'; import { GithubPromptHeaderAttributes } from './promptFileAttributes.js'; export const MARKERS_OWNER_ID = 'prompts-diagnostics-provider'; @@ -191,6 +192,7 @@ export class PromptValidator { this.validateUserInvokable(attributes, report); this.validateDisableModelInvocation(attributes, report); this.validateTools(attributes, ChatModeKind.Agent, target, report); + this.validateHooks(attributes, target, report); if (isVSCodeOrDefaultTarget(target)) { this.validateModel(attributes, ChatModeKind.Agent, report); this.validateHandoffs(attributes, report); @@ -545,6 +547,119 @@ export class PromptValidator { } } + private validateHooks(attributes: IHeaderAttribute[], target: Target, report: (markers: IMarkerData) => void): undefined { + const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.hooks); + if (!attribute) { + return; + } + if (attribute.value.type !== 'map') { + report(toMarker(localize('promptValidator.hooksMustBeMap', "The 'hooks' attribute must be a map of hook event types to command arrays."), attribute.value.range, MarkerSeverity.Error)); + return; + } + const validHookNames = new Set(Object.keys(HOOKS_BY_TARGET[target] ?? HOOKS_BY_TARGET[Target.Undefined])); + for (const prop of attribute.value.properties) { + if (!validHookNames.has(prop.key.value)) { + report(toMarker(localize('promptValidator.unknownHookType', "Unknown hook event type '{0}'. Supported: {1}.", prop.key.value, Array.from(validHookNames).join(', ')), prop.key.range, MarkerSeverity.Warning)); + } + if (prop.value.type !== 'sequence') { + report(toMarker(localize('promptValidator.hookValueMustBeArray', "Hook event '{0}' must have an array of command objects as its value.", prop.key.value), prop.value.range, MarkerSeverity.Error)); + continue; + } + for (const item of prop.value.items) { + this.validateHookCommand(item, target, report); + } + } + } + + private validateHookCommand(item: IValue, target: Target, report: (markers: IMarkerData) => void): void { + if (item.type !== 'map') { + report(toMarker(localize('promptValidator.hookCommandMustBeObject', "Each hook command must be an object."), item.range, MarkerSeverity.Error)); + return; + } + + // Detect nested matcher format: { matcher?: "...", hooks: [{ type: 'command', command: '...' }] } + const hooksProperty = item.properties.find(p => p.key.value === 'hooks'); + if (hooksProperty) { + // Validate that only known matcher properties are present + for (const prop of item.properties) { + if (prop.key.value !== 'hooks' && prop.key.value !== 'matcher') { + report(toMarker(localize('promptValidator.unknownMatcherProperty', "Unknown property '{0}' in hook matcher.", prop.key.value), prop.key.range, MarkerSeverity.Warning)); + } + } + if (hooksProperty.value.type !== 'sequence') { + report(toMarker(localize('promptValidator.nestedHooksMustBeArray', "The 'hooks' property in a matcher must be an array of command objects."), hooksProperty.value.range, MarkerSeverity.Error)); + return; + } + for (const nestedItem of hooksProperty.value.items) { + this.validateHookCommand(nestedItem, target, report); + } + return; + } + + const isCopilotCli = target === Target.GitHubCopilot; + + // Determine valid and command-providing properties based on target + const validCommandFields = isCopilotCli + ? new Set(['bash', 'powershell']) + : new Set(['command', 'windows', 'linux', 'osx', 'bash', 'powershell']); + + const validProperties = isCopilotCli + ? new Set(['type', 'bash', 'powershell', 'cwd', 'env', 'timeoutSec']) + : new Set(['type', 'command', 'windows', 'linux', 'osx', 'bash', 'powershell', 'cwd', 'env', 'timeout']); + + let hasType = false; + let hasCommandField = false; + + for (const prop of item.properties) { + const key = prop.key.value; + + if (!validProperties.has(key)) { + report(toMarker(localize('promptValidator.unknownHookProperty', "Unknown property '{0}' in hook command.", key), prop.key.range, MarkerSeverity.Warning)); + } + + if (key === 'type') { + hasType = true; + if (prop.value.type !== 'scalar' || prop.value.value !== 'command') { + report(toMarker(localize('promptValidator.hookTypeMustBeCommand', "The 'type' property in a hook command must be 'command'."), prop.value.range, MarkerSeverity.Error)); + } + } else if (validCommandFields.has(key)) { + hasCommandField = true; + if (prop.value.type !== 'scalar' || prop.value.value.trim().length === 0) { + report(toMarker(localize('promptValidator.hookCommandFieldMustBeNonEmptyString', "The '{0}' property in a hook command must be a non-empty string.", key), prop.value.range, MarkerSeverity.Error)); + } + } else if (key === 'cwd') { + if (prop.value.type !== 'scalar') { + report(toMarker(localize('promptValidator.hookCwdMustBeString', "The 'cwd' property in a hook command must be a string."), prop.value.range, MarkerSeverity.Error)); + } + } else if (key === 'env') { + if (prop.value.type !== 'map') { + report(toMarker(localize('promptValidator.hookEnvMustBeMap', "The 'env' property in a hook command must be a map of string values."), prop.value.range, MarkerSeverity.Error)); + } else { + for (const envProp of prop.value.properties) { + if (envProp.value.type !== 'scalar') { + report(toMarker(localize('promptValidator.hookEnvValueMustBeString', "Environment variable '{0}' must have a string value.", envProp.key.value), envProp.value.range, MarkerSeverity.Error)); + } + } + } + } else if (key === 'timeout' || key === 'timeoutSec') { + if (prop.value.type !== 'scalar' || isNaN(Number(prop.value.value))) { + report(toMarker(localize('promptValidator.hookTimeoutMustBeNumber', "The '{0}' property in a hook command must be a number.", key), prop.value.range, MarkerSeverity.Error)); + } + } + } + + if (!hasType) { + report(toMarker(localize('promptValidator.hookMissingType', "Hook command is missing required property 'type'."), item.range, MarkerSeverity.Error)); + } + if (!hasCommandField) { + if (isCopilotCli) { + report(toMarker(localize('promptValidator.hookMissingCopilotCommand', "Hook command must specify at least one of 'bash' or 'powershell'."), item.range, MarkerSeverity.Error)); + } else { + report(toMarker(localize('promptValidator.hookMissingCommand', "Hook command must specify at least one of 'command', 'windows', 'linux', or 'osx'."), item.range, MarkerSeverity.Error)); + } + } + } + private validateHandoffs(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): undefined { const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.handOffs); if (!attribute) { @@ -761,7 +876,7 @@ function isTrueOrFalse(value: IValue): boolean { const allAttributeNames: Record = { [PromptsType.prompt]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.mode, PromptHeaderAttributes.agent, PromptHeaderAttributes.argumentHint], [PromptsType.instructions]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.applyTo, PromptHeaderAttributes.excludeAgent], - [PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.infer, PromptHeaderAttributes.agents, PromptHeaderAttributes.userInvocable, PromptHeaderAttributes.userInvokable, PromptHeaderAttributes.disableModelInvocation, GithubPromptHeaderAttributes.github], + [PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.infer, PromptHeaderAttributes.agents, PromptHeaderAttributes.hooks, PromptHeaderAttributes.userInvocable, PromptHeaderAttributes.userInvokable, PromptHeaderAttributes.disableModelInvocation, GithubPromptHeaderAttributes.github], [PromptsType.skill]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.license, PromptHeaderAttributes.compatibility, PromptHeaderAttributes.metadata, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.userInvocable, PromptHeaderAttributes.userInvokable, PromptHeaderAttributes.disableModelInvocation], [PromptsType.hook]: [], // hooks are JSON files, not markdown with YAML frontmatter }; @@ -846,6 +961,8 @@ export function getAttributeDescription(attributeName: string, promptType: Promp return localize('promptHeader.agent.infer', 'Controls visibility of the agent.'); case PromptHeaderAttributes.agents: return localize('promptHeader.agent.agents', 'One or more agents that this agent can use as subagents. Use \'*\' to specify all available agents.'); + case PromptHeaderAttributes.hooks: + return localize('promptHeader.agent.hooks', 'Lifecycle hooks scoped to this agent. Define hooks that run only while this agent is active.'); case PromptHeaderAttributes.userInvocable: return localize('promptHeader.agent.userInvocable', 'Whether the agent can be selected and invoked by users in the UI.'); case PromptHeaderAttributes.disableModelInvocation: diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts index 3d04a7cfec313..240265152cffc 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts @@ -84,6 +84,7 @@ export namespace PromptHeaderAttributes { export const userInvokable = 'user-invokable'; export const userInvocable = 'user-invocable'; export const disableModelInvocation = 'disable-model-invocation'; + export const hooks = 'hooks'; } export class PromptHeader { @@ -317,6 +318,20 @@ export class PromptHeader { return this.getBooleanAttribute(PromptHeaderAttributes.disableModelInvocation); } + /** + * Gets the raw 'hooks' attribute value from the header. + * Returns the YAML map value if present, or undefined. The caller is + * responsible for converting this to `ChatRequestHooks` via + * {@link parseSubagentHooksFromYaml}. + */ + public get hooksRaw(): IMapValue | undefined { + const attr = this._parsedHeader.attributes.find(a => a.key === PromptHeaderAttributes.hooks); + if (attr?.value.type === 'map') { + return attr.value; + } + return undefined; + } + private getBooleanAttribute(key: string): boolean | undefined { const attribute = this._parsedHeader.attributes.find(attr => attr.key === key); if (attribute?.value.type === 'scalar') { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index b4de6bbc2e4c9..3e04c2fed58ed 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -225,6 +225,11 @@ export interface ICustomAgent { */ readonly agents?: readonly string[]; + /** + * Lifecycle hooks scoped to this subagent. + */ + readonly hooks?: ChatRequestHooks; + /** * Where the agent was loaded from. */ 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 c628f10fcf7ff..688ad88d715ca 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -36,7 +36,7 @@ import { PromptFileParser, ParsedPromptFile, PromptHeaderAttributes } from '../p import { IAgentInstructions, type IAgentSource, IChatPromptSlashCommand, IConfiguredHooksInfo, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPluginPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileContext, IPromptFileResource, PROMPT_FILE_PROVIDER_ACTIVATION_EVENT, SKILL_PROVIDER_ACTIVATION_EVENT, IPromptDiscoveryInfo, IPromptFileDiscoveryResult, IPromptSourceFolderResult, ICustomAgentVisibility, IResolvedAgentFile, AgentFileType, Logger, IPromptDiscoveryLogEntry } from './promptsService.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { Schemas } from '../../../../../../base/common/network.js'; -import { ChatRequestHooks, IHookCommand } from '../hookSchema.js'; +import { ChatRequestHooks, IHookCommand, parseSubagentHooksFromYaml } from '../hookSchema.js'; import { HookType } from '../hookTypes.js'; import { HookSourceFormat, getHookSourceFormat, parseHooksFromFile } from '../hookCompatibility.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; @@ -678,6 +678,12 @@ export class PromptsService extends Disposable implements IPromptsService { let agentFiles = await this.listPromptFiles(PromptsType.agent, token); const disabledAgents = this.getDisabledPromptFiles(PromptsType.agent); agentFiles = agentFiles.filter(promptPath => !disabledAgents.has(promptPath.uri)); + + // Get user home for tilde expansion in hook cwd paths + const userHomeUri = await this.pathService.userHome(); + const userHome = userHomeUri.scheme === Schemas.file ? userHomeUri.fsPath : userHomeUri.path; + const defaultFolder = this.workspaceService.getWorkspace().folders[0]; + const customAgentsResults = await Promise.allSettled( agentFiles.map(async (promptPath): Promise => { const uri = promptPath.uri; @@ -733,7 +739,17 @@ export class PromptsService extends Disposable implements IPromptsService { if (target === Target.Claude && tools) { tools = mapClaudeTools(tools); } - return { uri, name, description, model, tools, handOffs, argumentHint, target, visibility, agents, agentInstructions, source }; + + // Parse hooks from the frontmatter if present + let hooks: ChatRequestHooks | undefined; + const hooksRaw = ast.header.hooksRaw; + if (hooksRaw) { + const hookWorkspaceFolder = this.workspaceService.getWorkspaceFolder(uri) ?? defaultFolder; + const workspaceRootUri = hookWorkspaceFolder?.uri; + hooks = parseSubagentHooksFromYaml(hooksRaw, workspaceRootUri, userHome, target); + } + + return { uri, name, description, model, tools, handOffs, argumentHint, target, visibility, agents, hooks, agentInstructions, source }; }) ); diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts index 5cca23e27618e..7f7bb156f0707 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts @@ -13,6 +13,7 @@ import { localize } from '../../../../../../nls.js'; import { IChatQuestion, IChatService } from '../../chatService/chatService.js'; import { ChatQuestionCarouselData } from '../../model/chatProgressTypes/chatQuestionCarouselData.js'; import { IChatRequestModel } from '../../model/chatModel.js'; +import { ChatPermissionLevel } from '../../constants.js'; import { StopWatch } from '../../../../../../base/common/stopwatch.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; @@ -22,6 +23,12 @@ import { Codicon } from '../../../../../../base/common/codicons.js'; import { raceCancellation } from '../../../../../../base/common/async.js'; import { URI } from '../../../../../../base/common/uri.js'; +/** + * Response returned to the model when the user is not available (autopilot mode). + */ +export const AUTOPILOT_ASK_USER_RESPONSE = + 'The user is not available to respond and will review your work later. Work autonomously and make good decisions.'; + // Use a distinct id to avoid clashing with extension-provided tools export const AskQuestionsToolId = 'vscode_askQuestions'; @@ -53,22 +60,6 @@ function truncateToLimit(value: string | undefined, limit: number): string | und return value; } -export function formatHeaderForDisplay(header: string): string { - const normalized = header - .trim() - .replace(/[_-]+/g, ' ') - .replace(/([a-z0-9])([A-Z])/g, '$1 $2') - .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') - .replace(/\s+/g, ' ') - .trim(); - - if (!normalized) { - return header; - } - - return normalized.charAt(0).toUpperCase() + normalized.slice(1).toLowerCase(); -} - export interface IQuestionOption { readonly label: string; readonly description?: string; @@ -203,6 +194,17 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { return this.createSkippedResult(questions); } + // In autopilot mode, the user is not available — auto-respond instead of blocking. + // Still append a completed carousel so the user can see the auto-selected answers. + if (request.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot) { + this.logService.info('[AskQuestionsTool] Autopilot mode: auto-responding to questions'); + const { carousel, idToHeaderMap } = this.toQuestionCarousel(questions); + carousel.data = this.buildAutopilotCarouselAnswers(questions, carousel, idToHeaderMap); + carousel.isUsed = true; + this.chatService.appendProgress(request, carousel); + return this.createAutopilotResult(questions); + } + const { carousel, idToHeaderMap } = this.toQuestionCarousel(questions); this.chatService.appendProgress(request, carousel); @@ -211,7 +213,7 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { throw new CancellationError(); } - progress.report({ message: localize('askQuestionsTool.progress', 'Reviewing your answers') }); + progress.report({ message: localize('askQuestionsTool.progress', 'Analyzing your answers...') }); const converted = this.convertCarouselAnswers(questions, answerResult?.answers, idToHeaderMap); const { answeredCount, skippedCount, freeTextCount, recommendedAvailableCount, recommendedSelectedCount } = this.collectMetrics(questions, converted); @@ -316,9 +318,8 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { const internalId = generateUuid(); idToHeaderMap.set(internalId, question.header); - // Format + truncate header for display only; preserve original header for answer correlation - const formattedHeader = formatHeaderForDisplay(question.header); - const displayTitle = truncateToLimit(formattedHeader, HardLimits.header) ?? formattedHeader; + // Truncate header for display only + const displayTitle = truncateToLimit(question.header, HardLimits.header) ?? question.header; return { id: internalId, @@ -494,6 +495,63 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { }; } + private createAutopilotResult(questions: IQuestion[]): IToolResult { + const answers: Record = {}; + for (const question of questions) { + // Pick the recommended option if available, otherwise pick the first option + const recommended = question.options?.find(opt => opt.recommended); + const firstOption = question.options?.[0]; + const selected = recommended?.label ?? firstOption?.label; + answers[question.header] = { + selected: selected ? [selected] : [], + freeText: selected ? null : AUTOPILOT_ASK_USER_RESPONSE, + skipped: false, + }; + } + return { + content: [{ kind: 'text', value: JSON.stringify({ answers } satisfies IAnswerResult) }] + }; + } + + /** + * Build carousel answer data keyed by carousel question IDs for rendering + * the completed summary in the UI during autopilot mode. + */ + private buildAutopilotCarouselAnswers(questions: IQuestion[], carousel: ChatQuestionCarouselData, idToHeaderMap: Map): Record { + const data: Record = {}; + // Build reverse map: original header -> internal carousel question ID + const headerToIdMap = new Map(); + for (const [internalId, originalHeader] of idToHeaderMap) { + headerToIdMap.set(originalHeader, internalId); + } + + for (const question of questions) { + const internalId = headerToIdMap.get(question.header); + if (!internalId) { + continue; + } + + const chatQuestion = carousel.questions.find(q => q.id === internalId); + if (!chatQuestion) { + continue; + } + + const recommended = question.options?.find(opt => opt.recommended); + const firstOption = question.options?.[0]; + const selectedLabel = recommended?.label ?? firstOption?.label; + + if (chatQuestion.type === 'text' || !selectedLabel) { + data[internalId] = AUTOPILOT_ASK_USER_RESPONSE; + } else if (chatQuestion.type === 'multiSelect') { + data[internalId] = { selectedValues: [selectedLabel] }; + } else { + data[internalId] = { selectedValue: selectedLabel }; + } + } + + return data; + } + private sendTelemetry(requestId: string | undefined, questionCount: number, answeredCount: number, skippedCount: number, freeTextCount: number, recommendedAvailableCount: number, recommendedSelectedCount: number, duration: number): void { this.telemetryService.publicLog2('askQuestionsToolInvoked', { requestId, diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts index e5fd0a7a691a0..c5931a7d73213 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -23,7 +23,8 @@ import { ILanguageModelsService } from '../../languageModels.js'; import { ChatModel, IChatRequestModeInstructions } from '../../model/chatModel.js'; import { IChatAgentRequest, IChatAgentService } from '../../participants/chatAgents.js'; import { ComputeAutomaticInstructions } from '../../promptSyntax/computeAutomaticInstructions.js'; -import { ChatRequestHooks } from '../../promptSyntax/hookSchema.js'; +import { ChatRequestHooks, mergeHooks } from '../../promptSyntax/hookSchema.js'; +import { HookType } from '../../promptSyntax/hookTypes.js'; import { ICustomAgent, IPromptsService } from '../../promptSyntax/service/promptsService.js'; import { isBuiltinAgent } from '../../promptSyntax/utils/promptsServiceUtils.js'; import { @@ -260,6 +261,20 @@ export class RunSubagentTool extends Disposable implements IToolImpl { this.logService.warn('[ChatService] Failed to collect hooks:', error); } + // Merge subagent-level hooks (from the agent's frontmatter) with global hooks. + // Remap Stop hooks to SubagentStop since the agent is running as a subagent. + if (subagent?.hooks) { + const remapped: ChatRequestHooks = { ...subagent.hooks }; + if (remapped[HookType.Stop]) { + const stopHooks = remapped[HookType.Stop]; + (remapped as Record)[HookType.SubagentStop] = remapped[HookType.SubagentStop] + ? [...remapped[HookType.SubagentStop], ...stopHooks] + : stopHooks; + (remapped as Record)[HookType.Stop] = undefined; + } + collectedHooks = mergeHooks(collectedHooks, remapped); + } + // Build the agent request const agentRequest: IChatAgentRequest = { sessionResource: invocation.context.sessionResource, diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts new file mode 100644 index 0000000000000..74879d30d2111 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress, CountTokensCallback } from '../languageModelToolsService.js'; + +export const TaskCompleteToolId = 'task_complete'; + +/** + * Message sent to the agent when the session goes idle without task completion. + */ +export const AUTOPILOT_CONTINUATION_MESSAGE = + 'You have not yet marked the task as complete using the task_complete tool. ' + + 'You MUST call task_complete when done — whether the task involved code changes, answering a question, or any other interaction.\n\n' + + 'Do NOT repeat or restate your previous response. Pick up where you left off.\n\n' + + 'If you were planning, stop planning and start implementing. ' + + 'You are not done until you have fully completed the task.\n\n' + + 'IMPORTANT: Do NOT call task_complete if:\n' + + '- You have open questions or ambiguities — make good decisions and keep working\n' + + '- You encountered an error — try to resolve it or find an alternative approach\n' + + '- There are remaining steps — complete them first\n\n' + + 'Keep working autonomously until the task is truly finished, then call task_complete.'; + +export const TaskCompleteToolData: IToolData = { + id: TaskCompleteToolId, + displayName: 'Task Complete', + modelDescription: + 'Signal that the user\'s task is fully done. You MUST call this tool when your work is complete — ' + + 'whether you made code changes, answered a question, or completed any other kind of task. ' + + 'Provide a brief summary of what was accomplished. If the summary is trivial (e.g. answering a question), omit it. ' + + 'Do not restate the summary in your message text — it is shown to the user directly.\n\n' + + 'When to call:\n' + + '- After answering the user\'s question or completing a conversational request\n' + + '- After you have completed ALL requested changes\n' + + '- After verifying results: tests pass, terminal commands succeeded, tool calls returned expected output\n\n' + + 'When NOT to call:\n' + + '- If a terminal command failed or produced unexpected output\n' + + '- If an MCP or external tool call returned an error\n' + + '- If you encountered errors you have not resolved\n' + + '- If there are remaining steps to complete\n' + + '- If you have not verified your changes work', + source: ToolDataSource.Internal, + inputSchema: { + type: 'object', + properties: { + summary: { + type: 'string', + description: 'Brief summary of what was accomplished. Omit for trivial interactions.', + }, + }, + }, +}; + +export class TaskCompleteTool implements IToolImpl { + async prepareToolInvocation(_context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { + return { + presentation: ToolInvocationPresentation.Hidden, + }; + } + + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise { + const params = invocation.parameters as { summary?: string }; + const summary = params?.summary ?? 'All done!'; + return { + content: [{ + kind: 'text', + value: summary, + }], + }; + } +} diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts index 619e63406dd36..74f61e8b613b1 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts @@ -13,6 +13,7 @@ import { EditTool, EditToolData } from './editFileTool.js'; import { createManageTodoListToolData, ManageTodoListTool } from './manageTodoListTool.js'; import { ResolveDebugEventDetailsTool, ResolveDebugEventDetailsToolData } from './resolveDebugEventDetailsTool.js'; import { RunSubagentTool } from './runSubagentTool.js'; +import { TaskCompleteTool, TaskCompleteToolData } from './taskCompleteTool.js'; export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution { @@ -35,15 +36,19 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo const manageTodoListTool = this._register(instantiationService.createInstance(ManageTodoListTool)); this._register(toolsService.registerTool(todoToolData, manageTodoListTool)); - // Register the confirmation tool const confirmationTool = instantiationService.createInstance(ConfirmationTool); this._register(toolsService.registerTool(ConfirmationToolData, confirmationTool)); this._register(toolsService.registerTool(ConfirmationToolWithOptionsData, confirmationTool)); + + const taskCompleteTool = instantiationService.createInstance(TaskCompleteTool); + this._register(toolsService.registerTool(TaskCompleteToolData, taskCompleteTool)); + const resolveDebugEventDetailsTool = instantiationService.createInstance(ResolveDebugEventDetailsTool); this._register(toolsService.registerTool(ResolveDebugEventDetailsToolData, resolveDebugEventDetailsTool)); this._register(toolsService.readToolSet.addTool(ResolveDebugEventDetailsToolData)); + const runSubagentTool = this._register(instantiationService.createInstance(RunSubagentTool)); let runSubagentRegistration: IDisposable | undefined; diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/hookUtils.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/hookUtils.test.ts index d75ce8adbb8a9..33cbbd85c1aae 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/hookUtils.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/hookUtils.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { findHookCommandSelection } from '../../../browser/promptSyntax/hookUtils.js'; +import { findHookCommandInYaml, findHookCommandSelection } from '../../../browser/promptSyntax/hookUtils.js'; import { ITextEditorSelection } from '../../../../../../platform/editor/common/editor.js'; import { buildNewHookEntry, HookSourceFormat } from '../../../common/promptSyntax/hookCompatibility.js'; @@ -722,4 +722,232 @@ suite('hookUtils', () => { }); }); }); + + suite('findHookCommandInYaml', () => { + + test('finds unquoted command value', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ' - command: echo hello', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'echo hello'); + assert.deepStrictEqual(result, { + startLineNumber: 4, + startColumn: 16, + endLineNumber: 4, + endColumn: 26 + }); + }); + + test('finds double-quoted command value', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ' - command: "echo hello"', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'echo hello'); + }); + + test('finds single-quoted command value', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ` - command: 'echo hello'`, + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'echo hello'); + }); + + test('finds command without list prefix', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ' command: run-lint', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'run-lint'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'run-lint'); + }); + + test('does not match substring of a longer command', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ' - command: echo hello-world', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.strictEqual(result, undefined); + }); + + test('returns undefined when command is not found', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ' - command: echo hello', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo goodbye'); + assert.strictEqual(result, undefined); + }); + + test('returns undefined when no command lines exist', () => { + const content = [ + '---', + 'name: my-agent', + 'description: An agent', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.strictEqual(result, undefined); + }); + + test('returns undefined for empty content', () => { + const result = findHookCommandInYaml('', 'echo hello'); + assert.strictEqual(result, undefined); + }); + + test('finds first matching command when multiple exist', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ' - command: echo hello', + ' userPromptSubmit:', + ' - command: echo hello', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.ok(result); + assert.strictEqual(result.startLineNumber, 4); + }); + + test('ignores lines that are not command fields', () => { + const content = [ + '---', + 'description: run command echo hello', + 'hooks:', + ' sessionStart:', + ' - command: echo hello', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.ok(result); + assert.strictEqual(result.startLineNumber, 5); + }); + + test('handles command with special characters', () => { + const content = [ + '---', + 'hooks:', + ' preToolUse:', + ' - command: echo "foo" > /tmp/out.txt', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo "foo" > /tmp/out.txt'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'echo "foo" > /tmp/out.txt'); + }); + + test('matches command followed by trailing whitespace', () => { + const content = [ + '---', + 'hooks:', + ' sessionStart:', + ' - command: echo hello ', + '---', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'echo hello'); + }); + + test('finds short command that is a substring of the key name', () => { + const content = [ + 'hooks:', + ' Stop:', + ' - timeout: 10', + ' command: "a"', + ' type: command', + ].join('\n'); + const result = findHookCommandInYaml(content, 'a'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'a'); + assert.strictEqual(result.startLineNumber, 4); + }); + + test('finds short command in bash field that is a substring of the key name', () => { + const content = [ + 'hooks:', + ' sessionStart:', + ' - bash: "a"', + ' type: command', + ].join('\n'); + const result = findHookCommandInYaml(content, 'a'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'a'); + assert.strictEqual(result.startLineNumber, 3); + }); + + test('finds command in powershell field', () => { + const content = [ + 'hooks:', + ' sessionStart:', + ' - powershell: "echo hello"', + ' type: command', + ].join('\n'); + const result = findHookCommandInYaml(content, 'echo hello'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'echo hello'); + assert.strictEqual(result.startLineNumber, 3); + }); + + test('finds command in windows field', () => { + const content = [ + 'hooks:', + ' sessionStart:', + ' - windows: "dir"', + ' type: command', + ].join('\n'); + const result = findHookCommandInYaml(content, 'dir'); + assert.ok(result); + assert.strictEqual(getSelectedText(content, result), 'dir'); + assert.strictEqual(result.startLineNumber, 3); + }); + + test('finds command in linux and osx fields', () => { + const content = [ + 'hooks:', + ' sessionStart:', + ' - linux: "ls"', + ' osx: "ls -G"', + ' type: command', + ].join('\n'); + const linuxResult = findHookCommandInYaml(content, 'ls'); + assert.ok(linuxResult); + assert.strictEqual(getSelectedText(content, linuxResult), 'ls'); + assert.strictEqual(linuxResult.startLineNumber, 3); + + const osxResult = findHookCommandInYaml(content, 'ls -G'); + assert.ok(osxResult); + assert.strictEqual(getSelectedText(content, osxResult), 'ls -G'); + assert.strictEqual(osxResult.startLineNumber, 4); + }); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts index 1daf164ddcdd1..4ba131c89506a 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts @@ -143,6 +143,7 @@ suite('PromptHeaderAutocompletion', () => { { label: 'disable-model-invocation', result: 'disable-model-invocation: ${0:true}' }, { label: 'github', result: 'github: $0' }, { label: 'handoffs', result: 'handoffs: $0' }, + { label: 'hooks', result: 'hooks:\n ${1|SessionStart,SessionEnd,UserPromptSubmit,PreToolUse,PostToolUse,PreCompact,SubagentStart,SubagentStop,Stop,ErrorOccurred|}:\n - type: command\n command: "$2"' }, { label: 'model', result: 'model: ${0:MAE 4 (olama)}' }, { label: 'name', result: 'name: $0' }, { label: 'target', result: 'target: ${0:vscode}' }, @@ -390,6 +391,249 @@ suite('PromptHeaderAutocompletion', () => { const labels = actual.map(a => a.label); assert.ok(!labels.includes('BG Agent Model (copilot)'), 'Models with targetChatSessionType should be excluded from agent model array completions'); }); + + test('complete hooks value with New Hook snippet', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + assert.deepStrictEqual(actual, [ + { + label: 'New Hook', + result: 'hooks: \n ${1|SessionStart,SessionEnd,UserPromptSubmit,PreToolUse,PostToolUse,PreCompact,SubagentStart,SubagentStop,Stop,ErrorOccurred|}:\n - type: command\n command: "$2"' + }, + ]); + }); + + test('complete hooks value with New Hook snippet for vscode target', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + 'hooks: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + assert.deepStrictEqual(actual, [ + { + label: 'New Hook', + result: 'hooks: \n ${1|SessionStart,UserPromptSubmit,PreToolUse,PostToolUse,PreCompact,SubagentStart,SubagentStop,Stop|}:\n - type: command\n command: "$2"' + }, + ]); + }); + + test('complete hook event names inside hooks map', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: "echo hi"', + ' |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label).sort(); + // SessionStart should be excluded since it already exists + assert.ok(!labels.includes('SessionStart'), 'SessionStart should not be suggested when already present'); + assert.ok(labels.includes('SessionEnd'), 'SessionEnd should be suggested'); + assert.ok(labels.includes('PreToolUse'), 'PreToolUse should be suggested'); + assert.ok(labels.includes('Stop'), 'Stop should be suggested'); + }); + + test('complete hook event names for vscode target excludes existing hooks', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: "echo hi"', + ' PreToolUse:', + ' - type: command', + ' command: "lint"', + ' |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label).sort(); + assert.ok(!labels.includes('SessionStart'), 'SessionStart should not be suggested when already present'); + assert.ok(!labels.includes('PreToolUse'), 'PreToolUse should not be suggested when already present'); + assert.ok(labels.includes('UserPromptSubmit'), 'UserPromptSubmit should be suggested'); + assert.ok(labels.includes('PostToolUse'), 'PostToolUse should be suggested'); + // SessionEnd is not available for vscode target + assert.ok(!labels.includes('SessionEnd'), 'SessionEnd should not be available for vscode target'); + }); + + test('complete hook event names on empty line before existing hooks', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' |', + ' SessionStart:', + ' - type: command', + ' command: "echo hi"', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label).sort(); + assert.ok(!labels.includes('SessionStart'), 'SessionStart should not be suggested when already present'); + assert.ok(labels.includes('SessionEnd'), 'SessionEnd should be suggested'); + assert.ok(labels.includes('PreToolUse'), 'PreToolUse should be suggested'); + }); + + test('complete hook event names while editing existing key name', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' S|:', + ' - type: command', + ' command: "echo hi"', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label).sort(); + assert.ok(labels.includes('SessionStart'), 'SessionStart should be suggested'); + assert.ok(labels.includes('SubagentStart'), 'SubagentStart should be suggested'); + assert.ok(labels.includes('Stop'), 'Stop should be suggested'); + // Verify insertText only replaces the key (no full snippet) + const sessionStartItem = actual.find(a => a.label === 'SessionStart'); + assert.ok(sessionStartItem); + assert.strictEqual(sessionStartItem.result, ' SessionStart:'); + }); + + test('hooks: cursor right after colon triggers New Hook snippet', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + assert.ok(labels.includes('New Hook'), 'New Hook snippet should be suggested'); + }); + + test('hooks: typing event name on next line triggers hook events', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' S|', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + assert.ok(labels.includes('SessionStart'), 'SessionStart should be suggested'); + assert.ok(labels.includes('SessionEnd'), 'SessionEnd should be suggested'); + assert.ok(labels.includes('Stop'), 'Stop should be suggested'); + }); + + test('typing field name in first command entry triggers command fields', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionEnd:', + ' - t|', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + assert.ok(labels.includes('type'), 'type should be suggested'); + assert.ok(labels.includes('command'), 'command should be suggested'); + assert.ok(labels.includes('timeout'), 'timeout should be suggested'); + }); + + test('typing field name after existing field triggers remaining command fields', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionEnd:', + ' - type: command', + ' c|', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + assert.ok(labels.includes('command'), 'command should be suggested'); + assert.ok(labels.includes('cwd'), 'cwd should be suggested'); + assert.ok(!labels.includes('type'), 'type should not be suggested when already present'); + }); + + test('typing event name after existing hook triggers hook events', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionEnd:', + ' - type: command', + ' command: echo "Session ended."', + ' U|', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + assert.ok(labels.includes('UserPromptSubmit'), 'UserPromptSubmit should be suggested'); + assert.ok(!labels.includes('SessionEnd'), 'SessionEnd should not be suggested when already present'); + }); + + test('typing event name between existing hooks triggers hook events', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionEnd:', + ' - type: command', + ' command: echo "Session ended."', + ' S|', + ' UserPromptSubmit:', + ' - type: command', + ' command: echo "User submitted."', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + assert.ok(labels.includes('SessionStart'), 'SessionStart should be suggested'); + assert.ok(labels.includes('Stop'), 'Stop should be suggested'); + assert.ok(!labels.includes('SessionEnd'), 'SessionEnd should not be suggested when already present'); + assert.ok(!labels.includes('UserPromptSubmit'), 'UserPromptSubmit should not be suggested when already present'); + }); + + test('cursor after hook event colon triggers New Command snippet', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionEnd: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + const labels = actual.map(a => a.label); + assert.ok(labels.includes('New Command'), 'New Command snippet should be suggested'); + assert.strictEqual(actual.length, 1, 'Only one suggestion should be returned'); + }); }); suite('claude agent header completions', () => { diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts index cd985b7ba74f8..e1ad97bb79165 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts @@ -551,7 +551,7 @@ suite('PromptValidator', () => { assert.deepStrictEqual( markers.map(m => ({ severity: m.severity, message: m.message })), [ - { severity: MarkerSeverity.Warning, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: agents, argument-hint, description, disable-model-invocation, github, handoffs, model, name, target, tools, user-invocable.` }, + { severity: MarkerSeverity.Warning, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: agents, argument-hint, description, disable-model-invocation, github, handoffs, hooks, model, name, target, tools, user-invocable.` }, ] ); }); @@ -1416,6 +1416,358 @@ suite('PromptValidator', () => { assert.strictEqual(markers[0].message, `The 'disable-model-invocation' attribute must be 'true' or 'false'.`); } }); + + test('hooks - valid hook commands', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: echo hello', + ' PreToolUse:', + ' - type: command', + ' command: ./validate.sh', + ' cwd: scripts', + ' timeout: 30', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, []); + }); + + test('hooks - must be a map', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks: invalid', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'hooks' attribute must be a map of hook event types to command arrays.` }, + ] + ); + }); + + test('hooks - unknown hook event type', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' UnknownEvent:', + ' - type: command', + ' command: echo hello', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Warning, message: `Unknown hook event type 'UnknownEvent'. Supported: SessionStart, SessionEnd, UserPromptSubmit, PreToolUse, PostToolUse, PreCompact, SubagentStart, SubagentStop, Stop, ErrorOccurred.` }, + ] + ); + }); + + test('hooks - hook value must be array', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart: invalid', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `Hook event 'SessionStart' must have an array of command objects as its value.` }, + ] + ); + }); + + test('hooks - command item must be object', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - just a string', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `Each hook command must be an object.` }, + ] + ); + }); + + test('hooks - missing type property', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - command: echo hello', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `Hook command is missing required property 'type'.` }, + ] + ); + }); + + test('hooks - type must be command', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: script', + ' command: echo hello', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'type' property in a hook command must be 'command'.` }, + ] + ); + }); + + test('hooks - missing command field', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `Hook command must specify at least one of 'command', 'windows', 'linux', or 'osx'.` }, + ] + ); + }); + + test('hooks - empty command string', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: ""', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'command' property in a hook command must be a non-empty string.` }, + ] + ); + }); + + test('hooks - platform-specific commands are valid', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' windows: echo hello', + ' linux: echo hello', + ' osx: echo hello', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, []); + }); + + test('hooks - env must be a map with string values', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: echo hello', + ' env: invalid', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'env' property in a hook command must be a map of string values.` }, + ] + ); + }); + + test('hooks - valid env map', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: echo hello', + ' env:', + ' NODE_ENV: production', + ' DEBUG: "true"', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, []); + }); + + test('hooks - unknown property warns', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: echo hello', + ' unknownProp: value', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Warning, message: `Unknown property 'unknownProp' in hook command.` }, + ] + ); + }); + + test('hooks - timeout must be number', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: echo hello', + ' timeout: not-a-number', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'timeout' property in a hook command must be a number.` }, + ] + ); + }); + + test('hooks - cwd must be string', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: command', + ' command: echo hello', + ' cwd:', + ' - array', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'cwd' property in a hook command must be a string.` }, + ] + ); + }); + + test('hooks - multiple errors in one command', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' SessionStart:', + ' - type: script', + ' unknownProp: value', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'type' property in a hook command must be 'command'.` }, + { severity: MarkerSeverity.Warning, message: `Unknown property 'unknownProp' in hook command.` }, + { severity: MarkerSeverity.Error, message: `Hook command must specify at least one of 'command', 'windows', 'linux', or 'osx'.` }, + ] + ); + }); + + test('hooks - nested matcher format is valid', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' UserPromptSubmit:', + ' - hooks:', + ' - type: command', + ' command: "echo foo"', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, []); + }); + + test('hooks - nested matcher validates inner commands', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' PreToolUse:', + ' - matcher: Bash', + ' hooks:', + ' - type: script', + ' command: "echo foo"', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'type' property in a hook command must be 'command'.` }, + ] + ); + }); + + test('hooks - nested hooks must be array', async () => { + const content = [ + '---', + 'description: "Test"', + 'hooks:', + ' PreToolUse:', + ' - hooks: invalid', + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'hooks' property in a matcher must be an array of command objects.` }, + ] + ); + }); }); suite('instructions', () => { diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts index f6520b663505c..919df6c489386 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts @@ -58,6 +58,7 @@ suite('ChatQuestionCarouselPart', () => { createWidget(carousel); assert.ok(widget.domNode.classList.contains('chat-question-carousel-container')); + assert.ok(widget.domNode.querySelector('.chat-question-header-row')); assert.ok(widget.domNode.querySelector('.chat-question-carousel-content')); }); @@ -98,12 +99,7 @@ suite('ChatQuestionCarouselPart', () => { const title = widget.domNode.querySelector('.chat-question-title'); assert.ok(title, 'title element should exist'); - const messageEl = widget.domNode.querySelector('.chat-question-message'); - assert.ok(messageEl, 'message element should exist'); - assert.ok(messageEl?.querySelector('.rendered-markdown'), 'markdown content should be rendered'); - assert.strictEqual(messageEl?.textContent?.includes('**details**'), false, 'markdown syntax should not be shown as raw text'); - const link = messageEl?.querySelector('a') as HTMLAnchorElement | null; - assert.ok(link, 'markdown link should render as anchor'); + assert.ok(title?.querySelector('.rendered-markdown'), 'markdown content should be rendered'); }); test('renders plain string question message as text', () => { @@ -117,13 +113,12 @@ suite('ChatQuestionCarouselPart', () => { ]); createWidget(carousel); - const messageEl = widget.domNode.querySelector('.chat-question-message'); - assert.ok(messageEl, 'message element should exist'); - assert.ok(messageEl?.textContent?.includes('details'), 'plain text content should be rendered'); - assert.strictEqual(messageEl?.querySelector('.rendered-markdown'), null, 'plain string message should not use markdown renderer'); + const title = widget.domNode.querySelector('.chat-question-title'); + assert.ok(title, 'title element should exist'); + assert.ok(title?.textContent?.includes('details'), 'content should be rendered'); }); - test('renders tab bar for multi-question carousel', () => { + test('renders progress indicator correctly', () => { const carousel = createMockCarousel([ { id: 'q1', type: 'text', title: 'Question 1', message: 'Question 1' }, { id: 'q2', type: 'text', title: 'Question 2', message: 'Question 2' }, @@ -131,11 +126,11 @@ suite('ChatQuestionCarouselPart', () => { ]); createWidget(carousel); - const tabBar = widget.domNode.querySelector('.chat-question-tab-bar'); - assert.ok(tabBar, 'Tab bar should exist for multi-question carousel'); - const tabs = widget.domNode.querySelectorAll('.chat-question-tab'); - // 3 question tabs + 1 review tab - assert.strictEqual(tabs.length, 4, 'Should have 3 question tabs + 1 review tab'); + // Progress is shown in the step indicator in the footer as "1/3" + const stepIndicator = widget.domNode.querySelector('.chat-question-step-indicator'); + assert.ok(stepIndicator); + assert.ok(stepIndicator?.textContent?.includes('1')); + assert.ok(stepIndicator?.textContent?.includes('3')); }); }); @@ -269,16 +264,48 @@ suite('ChatQuestionCarouselPart', () => { }); suite('Navigation', () => { - test('single question has no tab bar or submit button', () => { + test('previous button is disabled on first question', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1' }, + { id: 'q2', type: 'text', title: 'Question 2' } + ]); + createWidget(carousel); + + const navArrows = widget.domNode.querySelectorAll('.chat-question-nav-arrow') as NodeListOf; + const prevButton = navArrows[0]; + assert.ok(prevButton, 'Previous button should exist'); + assert.ok(prevButton.classList.contains('disabled') || prevButton.disabled, 'Previous button should be disabled on first question'); + }); + + test('next button stays as arrow and is disabled on last question', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Only Question' }, + { id: 'q2', type: 'text', title: 'Question 2' } + ]); + createWidget(carousel); + + // Navigate to last question + widget.navigateToNextQuestion(); + + const navArrows = widget.domNode.querySelectorAll('.chat-question-nav-arrow') as NodeListOf; + const nextButton = navArrows[1]; + assert.ok(nextButton, 'Next button should exist'); + assert.ok(nextButton.classList.contains('disabled') || nextButton.disabled, 'Next button should be disabled on last question'); + }); + + test('submit button is shown on last question', () => { const carousel = createMockCarousel([ - { id: 'q1', type: 'text', title: 'Only Question' } + { id: 'q1', type: 'text', title: 'Question 1' }, + { id: 'q2', type: 'text', title: 'Question 2' } ]); createWidget(carousel); - const tabBar = widget.domNode.querySelector('.chat-question-tab-bar'); - assert.strictEqual(tabBar, null, 'Tab bar should not exist for single question'); - const submitButton = widget.domNode.querySelector('.chat-question-submit-button'); - assert.strictEqual(submitButton, null, 'Submit button is only in review panel for multi-question'); + // Navigate to last question + widget.navigateToNextQuestion(); + + const submitButton = widget.domNode.querySelector('.chat-question-submit-button') as HTMLButtonElement; + assert.ok(submitButton, 'Submit button should exist'); + assert.notStrictEqual(submitButton.style.display, 'none', 'Submit button should be visible on last question'); }); }); @@ -373,14 +400,13 @@ suite('ChatQuestionCarouselPart', () => { suite('Accessibility', () => { test('navigation area has proper role and aria-label', () => { const carousel = createMockCarousel([ - { id: 'q1', type: 'text', title: 'Question 1' }, - { id: 'q2', type: 'text', title: 'Question 2' } + { id: 'q1', type: 'text', title: 'Question 1' } ]); createWidget(carousel); - const tabList = widget.domNode.querySelector('.chat-question-tabs'); - assert.strictEqual(tabList?.getAttribute('role'), 'tablist'); - assert.ok(tabList?.getAttribute('aria-label'), 'Tab list should have aria-label'); + const nav = widget.domNode.querySelector('.chat-question-carousel-nav'); + assert.strictEqual(nav?.getAttribute('role'), 'navigation'); + assert.ok(nav?.getAttribute('aria-label'), 'Navigation should have aria-label'); }); test('single select list has proper role and aria-label', () => { @@ -559,20 +585,19 @@ suite('ChatQuestionCarouselPart', () => { ], true); const firstWidget = createWidget(carousel); - // Click the second tab to navigate - const tabs = firstWidget.domNode.querySelectorAll('.chat-question-tab'); - assert.ok(tabs.length >= 2, 'should have at least 2 tabs'); - (tabs[1] as HTMLElement).click(); - - // Verify navigation happened - assert.strictEqual(tabs[1].getAttribute('aria-selected'), 'true', 'second tab should be selected after click'); + const nextButton = firstWidget.domNode.querySelector('.chat-question-nav-next') as HTMLElement | null; + assert.ok(nextButton, 'next button should exist'); + nextButton.dispatchEvent(new MouseEvent('click', { bubbles: true })); firstWidget.dispose(); firstWidget.domNode.remove(); const recreatedWidget = createWidget(carousel); - const recreatedTabs = recreatedWidget.domNode.querySelectorAll('.chat-question-tab'); - assert.strictEqual(recreatedTabs[1]?.getAttribute('aria-selected'), 'true', 'should restore to second tab after recreation'); + const stepIndicator = recreatedWidget.domNode.querySelector('.chat-question-step-indicator'); + assert.strictEqual(stepIndicator?.textContent, '2/2', 'should restore the current question index after navigation'); + + const title = recreatedWidget.domNode.querySelector('.chat-question-title'); + assert.ok(title?.textContent?.includes('Question 2'), 'should restore to the second question view'); }); test('retains draft answers and current question after widget recreation', () => { @@ -587,9 +612,9 @@ suite('ChatQuestionCarouselPart', () => { firstInput.value = 'first draft answer'; firstInput.dispatchEvent(new Event('input', { bubbles: true })); - // Click the second tab to navigate - const tabs = firstWidget.domNode.querySelectorAll('.chat-question-tab'); - (tabs[1] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); + const nextButton = firstWidget.domNode.querySelector('.chat-question-nav-next') as HTMLElement | null; + assert.ok(nextButton, 'next button should exist'); + nextButton.dispatchEvent(new MouseEvent('click', { bubbles: true })); const secondInput = firstWidget.domNode.querySelector('.monaco-inputbox input') as HTMLInputElement | null; assert.ok(secondInput, 'second question input should exist'); @@ -600,16 +625,16 @@ suite('ChatQuestionCarouselPart', () => { firstWidget.domNode.remove(); const recreatedWidget = createWidget(carousel); - const recreatedTabs = recreatedWidget.domNode.querySelectorAll('.chat-question-tab'); - assert.strictEqual(recreatedTabs[1]?.getAttribute('aria-selected'), 'true', 'should restore the current question index'); + const stepIndicator = recreatedWidget.domNode.querySelector('.chat-question-step-indicator'); + assert.strictEqual(stepIndicator?.textContent, '2/2', 'should restore the current question index'); const recreatedSecondInput = recreatedWidget.domNode.querySelector('.monaco-inputbox input') as HTMLInputElement | null; assert.ok(recreatedSecondInput, 'recreated second question input should exist'); assert.strictEqual(recreatedSecondInput.value, 'second draft answer', 'should restore draft input for current question'); - // Click the first tab to go back - const recreatedTabsAgain = recreatedWidget.domNode.querySelectorAll('.chat-question-tab'); - (recreatedTabsAgain[0] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true })); + const prevButton = recreatedWidget.domNode.querySelector('.chat-question-nav-prev') as HTMLElement | null; + assert.ok(prevButton, 'previous button should exist'); + prevButton.dispatchEvent(new MouseEvent('click', { bubbles: true })); const recreatedFirstInput = recreatedWidget.domNode.querySelector('.monaco-inputbox input') as HTMLInputElement | null; assert.ok(recreatedFirstInput, 'recreated first question input should exist'); @@ -629,7 +654,7 @@ suite('ChatQuestionCarouselPart', () => { assert.ok(summary, 'Should show summary container after skip'); const summaryItem = summary?.querySelector('.chat-question-summary-item'); assert.ok(summaryItem, 'Should have summary item for the question'); - const summaryValue = summaryItem?.querySelector('.chat-question-summary-answer'); + const summaryValue = summaryItem?.querySelector('.chat-question-summary-answer-title'); assert.ok(summaryValue?.textContent?.includes('default answer'), 'Summary should show the default answer'); }); @@ -663,7 +688,7 @@ suite('ChatQuestionCarouselPart', () => { assert.ok(widget.domNode.classList.contains('chat-question-carousel-used'), 'Should have used class'); const summary = widget.domNode.querySelector('.chat-question-carousel-summary'); assert.ok(summary, 'Should show summary container when isUsed is true'); - const summaryValue = summary?.querySelector('.chat-question-summary-answer'); + const summaryValue = summary?.querySelector('.chat-question-summary-answer-title'); assert.ok(summaryValue?.textContent?.includes('saved answer'), 'Summary should show saved answer from data'); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts new file mode 100644 index 0000000000000..d282483d7d765 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatModelSelectionLogic.test.ts @@ -0,0 +1,1548 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { ExtensionIdentifier } from '../../../../../../../platform/extensions/common/extensions.js'; +import { ChatAgentLocation, ChatModeKind } from '../../../../common/constants.js'; +import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier } from '../../../../common/languageModels.js'; +import { + filterModelsForSession, + findDefaultModel, + hasModelsTargetingSession, + isModelSupportedForInlineChat, + isModelSupportedForMode, + isModelValidForSession, + mergeModelsWithCache, + resolveModelFromSyncState, + shouldResetModelToDefault, + shouldResetOnModelListChange, + shouldRestoreLateArrivingModel, + shouldRestorePersistedModel, +} from '../../../../browser/widget/input/chatModelSelectionLogic.js'; + +/** + * Test helper that composes the full startup pipeline: merge live+cache → sort → filter by session/mode. + * This mirrors what `chatInputPart.getModels()` does, but without the storage side effects. + */ +function computeAvailableModels( + liveModels: ILanguageModelChatMetadataAndIdentifier[], + cachedModels: ILanguageModelChatMetadataAndIdentifier[], + contributedVendors: Set, + sessionType: string | undefined, + currentModeKind: ChatModeKind, + location: ChatAgentLocation, + isInlineChatV2Enabled: boolean, +): ILanguageModelChatMetadataAndIdentifier[] { + const merged = mergeModelsWithCache(liveModels, cachedModels, contributedVendors); + merged.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)); + return filterModelsForSession(merged, sessionType, currentModeKind, location, isInlineChatV2Enabled); +} + +function createModel( + id: string, + name: string, + overrides?: Partial, +): ILanguageModelChatMetadataAndIdentifier { + return { + identifier: `copilot/${id}`, + metadata: { + extension: new ExtensionIdentifier('test.ext'), + id, + name, + vendor: 'copilot', + version: '1.0', + family: 'copilot', + maxInputTokens: 128000, + maxOutputTokens: 4096, + isDefaultForLocation: {}, + isUserSelectable: true, + modelPickerCategory: undefined, + capabilities: { toolCalling: true, agentMode: true }, + ...overrides, + } as ILanguageModelChatMetadata, + }; +} + +function createDefaultModelForLocation( + id: string, + name: string, + location: ChatAgentLocation, + overrides?: Partial, +): ILanguageModelChatMetadataAndIdentifier { + return createModel(id, name, { + isDefaultForLocation: { [location]: true }, + ...overrides, + }); +} + +function createSessionModel( + id: string, + name: string, + sessionType: string, + overrides?: Partial, +): ILanguageModelChatMetadataAndIdentifier { + return createModel(id, name, { + targetChatSessionType: sessionType, + ...overrides, + }); +} + +suite('ChatModelSelectionLogic', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('isModelSupportedForMode', () => { + + test('any model is supported in Ask mode', () => { + const model = createModel('basic', 'Basic', { capabilities: undefined }); + assert.strictEqual(isModelSupportedForMode(model, ChatModeKind.Ask), true); + }); + + test('any model is supported in Edit mode', () => { + const model = createModel('basic', 'Basic', { capabilities: undefined }); + assert.strictEqual(isModelSupportedForMode(model, ChatModeKind.Edit), true); + }); + + test('model with tool calling and agent mode is supported in Agent mode', () => { + const model = createModel('agent-capable', 'Agent-Capable', { + capabilities: { toolCalling: true, agentMode: true }, + }); + assert.strictEqual(isModelSupportedForMode(model, ChatModeKind.Agent), true); + }); + + test('model with tool calling but agentMode=undefined is supported in Agent mode', () => { + const model = createModel('tool-only', 'Tool-Only', { + capabilities: { toolCalling: true }, + }); + assert.strictEqual(isModelSupportedForMode(model, ChatModeKind.Agent), true); + }); + + test('model without tool calling is NOT supported in Agent mode', () => { + const model = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false }, + }); + assert.strictEqual(isModelSupportedForMode(model, ChatModeKind.Agent), false); + }); + + test('model with agentMode=false is NOT supported in Agent mode', () => { + const model = createModel('no-agent', 'No-Agent', { + capabilities: { toolCalling: true, agentMode: false }, + }); + assert.strictEqual(isModelSupportedForMode(model, ChatModeKind.Agent), false); + }); + + test('model with no capabilities is NOT supported in Agent mode', () => { + const model = createModel('no-caps', 'No-Caps', { capabilities: undefined }); + assert.strictEqual(isModelSupportedForMode(model, ChatModeKind.Agent), false); + }); + }); + + suite('isModelSupportedForInlineChat', () => { + + test('any model is supported when not in EditorInline location', () => { + const model = createModel('basic', 'Basic', { capabilities: undefined }); + assert.strictEqual(isModelSupportedForInlineChat(model, ChatAgentLocation.Chat, true), true); + assert.strictEqual(isModelSupportedForInlineChat(model, ChatAgentLocation.Terminal, true), true); + assert.strictEqual(isModelSupportedForInlineChat(model, ChatAgentLocation.Notebook, true), true); + }); + + test('any model is supported in EditorInline when V2 is disabled', () => { + const model = createModel('basic', 'Basic', { capabilities: undefined }); + assert.strictEqual(isModelSupportedForInlineChat(model, ChatAgentLocation.EditorInline, false), true); + }); + + test('model with tool calling is supported in EditorInline with V2', () => { + const model = createModel('tools', 'Tools', { + capabilities: { toolCalling: true }, + }); + assert.strictEqual(isModelSupportedForInlineChat(model, ChatAgentLocation.EditorInline, true), true); + }); + + test('model without tool calling is NOT supported in EditorInline with V2', () => { + const model = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false }, + }); + assert.strictEqual(isModelSupportedForInlineChat(model, ChatAgentLocation.EditorInline, true), false); + }); + + test('model with no capabilities is NOT supported in EditorInline with V2', () => { + const model = createModel('no-caps', 'No-Caps', { capabilities: undefined }); + assert.strictEqual(isModelSupportedForInlineChat(model, ChatAgentLocation.EditorInline, true), false); + }); + }); + + suite('filterModelsForSession', () => { + + const gpt4o = createModel('gpt-4o', 'GPT-4o'); + const claude = createModel('claude', 'Claude'); + const notSelectable = createModel('hidden', 'Hidden', { isUserSelectable: false }); + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const noToolsModel = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false, agentMode: false }, + }); + + test('returns user-selectable general models when no session type set', () => { + const result = filterModelsForSession( + [gpt4o, claude, notSelectable], + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-4o', 'claude']); + }); + + test('returns user-selectable general models for local session type', () => { + const result = filterModelsForSession( + [gpt4o, claude, notSelectable], + 'local', + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-4o', 'claude']); + }); + + test('excludes models targeting a specific session type when in general session', () => { + const result = filterModelsForSession( + [gpt4o, claude, cloudModel], + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-4o', 'claude']); + }); + + test('returns only session-targeted models for a specific session type', () => { + const result = filterModelsForSession( + [gpt4o, claude, cloudModel], + 'cloud', + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['cloud-gpt']); + }); + + test('filters out models incompatible with Agent mode in general session', () => { + const result = filterModelsForSession( + [gpt4o, noToolsModel], + undefined, + ChatModeKind.Agent, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-4o']); + }); + + test('filters by mode for session-targeted models', () => { + const cloudNoTools = createSessionModel('cloud-basic', 'Cloud Basic', 'cloud', { + capabilities: { toolCalling: false, agentMode: false }, + }); + const result = filterModelsForSession( + [gpt4o, cloudModel, cloudNoTools], + 'cloud', + ChatModeKind.Agent, + ChatAgentLocation.Chat, + false, + ); + // Session-type filtering also checks mode and inline chat support + assert.deepStrictEqual(result.map(m => m.metadata.id), ['cloud-gpt']); + }); + + test('excludes non-selectable models from session-targeted results', () => { + const cloudHidden = createSessionModel('cloud-hidden', 'Cloud Hidden', 'cloud', { + isUserSelectable: false, + }); + const result = filterModelsForSession( + [cloudModel, cloudHidden], + 'cloud', + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['cloud-gpt']); + }); + + test('falls back to general models when no models target the session type', () => { + const result = filterModelsForSession( + [gpt4o, claude], + 'cloud', + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-4o', 'claude']); + }); + + test('filters inline chat incompatible models in EditorInline with V2', () => { + const noToolsSelectable = createModel('no-tools-selectable', 'No-Tools-Selectable', { + capabilities: { toolCalling: false }, + }); + const result = filterModelsForSession( + [gpt4o, noToolsSelectable], + undefined, + ChatModeKind.Ask, + ChatAgentLocation.EditorInline, + true, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-4o']); + }); + }); + + suite('hasModelsTargetingSession', () => { + + test('returns false when session type is undefined', () => { + const models = [createModel('gpt', 'GPT')]; + assert.strictEqual(hasModelsTargetingSession(models, undefined), false); + }); + + test('returns false when no models target the session type', () => { + const models = [createModel('gpt', 'GPT')]; + assert.strictEqual(hasModelsTargetingSession(models, 'cloud'), false); + }); + + test('returns true when a model targets the session type', () => { + const models = [ + createModel('gpt', 'GPT'), + createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'), + ]; + assert.strictEqual(hasModelsTargetingSession(models, 'cloud'), true); + }); + + test('returns false for different session type', () => { + const models = [createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud')]; + assert.strictEqual(hasModelsTargetingSession(models, 'enterprise'), false); + }); + }); + + suite('isModelValidForSession', () => { + + test('general model is valid when no models target the session', () => { + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel]; + assert.strictEqual(isModelValidForSession(generalModel, allModels, 'cloud'), true); + }); + + test('session-targeted model is NOT valid when no models target the session type in pool', () => { + const sessionModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + assert.strictEqual(isModelValidForSession(sessionModel, [generalModel], undefined), false); + }); + + test('session-targeted model IS valid when pool has models targeting that session', () => { + const sessionModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const allModels = [createModel('gpt', 'GPT'), sessionModel]; + assert.strictEqual(isModelValidForSession(sessionModel, allModels, 'cloud'), true); + }); + + test('general model is NOT valid when pool has models targeting the session', () => { + const generalModel = createModel('gpt', 'GPT'); + const sessionModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const allModels = [generalModel, sessionModel]; + assert.strictEqual(isModelValidForSession(generalModel, allModels, 'cloud'), false); + }); + + test('model targeting wrong session is NOT valid', () => { + const wrongSessionModel = createSessionModel('ent-gpt', 'Enterprise GPT', 'enterprise'); + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const allModels = [wrongSessionModel, cloudModel]; + assert.strictEqual(isModelValidForSession(wrongSessionModel, allModels, 'cloud'), false); + }); + + test('general model is valid when session type is undefined', () => { + const generalModel = createModel('gpt', 'GPT'); + assert.strictEqual(isModelValidForSession(generalModel, [generalModel], undefined), true); + }); + }); + + suite('findDefaultModel', () => { + + test('returns model marked as default for location', () => { + const regular = createModel('gpt', 'GPT'); + const defaultModel = createDefaultModelForLocation('claude', 'Claude', ChatAgentLocation.Chat); + const result = findDefaultModel([regular, defaultModel], ChatAgentLocation.Chat); + assert.strictEqual(result?.metadata.id, 'claude'); + }); + + test('falls back to first model when no default for location', () => { + const modelA = createModel('gpt', 'GPT'); + const modelB = createModel('claude', 'Claude'); + const result = findDefaultModel([modelA, modelB], ChatAgentLocation.Chat); + assert.strictEqual(result?.metadata.id, 'gpt'); + }); + + test('returns undefined for empty models array', () => { + const result = findDefaultModel([], ChatAgentLocation.Chat); + assert.strictEqual(result, undefined); + }); + + test('returns location-specific default when multiple defaults exist', () => { + const chatDefault = createDefaultModelForLocation('chat-default', 'Chat Default', ChatAgentLocation.Chat); + const terminalDefault = createDefaultModelForLocation('terminal-default', 'Terminal Default', ChatAgentLocation.Terminal); + const result = findDefaultModel([chatDefault, terminalDefault], ChatAgentLocation.Chat); + assert.strictEqual(result?.metadata.id, 'chat-default'); + }); + + test('does not pick terminal default when looking for chat default', () => { + const terminalDefault = createDefaultModelForLocation('terminal-default', 'Terminal Default', ChatAgentLocation.Terminal); + const regular = createModel('gpt', 'GPT'); + const result = findDefaultModel([terminalDefault, regular], ChatAgentLocation.Chat); + // Falls back to first model since none is default for Chat + assert.strictEqual(result?.metadata.id, 'terminal-default'); + }); + }); + + suite('shouldRestorePersistedModel', () => { + + test('restores model that was explicitly chosen (not default)', () => { + const model = createModel('gpt', 'GPT'); + const result = shouldRestorePersistedModel('copilot/gpt', false, [model], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, true); + assert.strictEqual(result.model?.identifier, 'copilot/gpt'); + }); + + test('restores model that was default and is still default', () => { + const model = createDefaultModelForLocation('gpt', 'GPT', ChatAgentLocation.Chat); + const result = shouldRestorePersistedModel('copilot/gpt', true, [model], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, true); + }); + + test('does NOT restore model that was default but is no longer default', () => { + const model = createModel('gpt', 'GPT'); + const result = shouldRestorePersistedModel('copilot/gpt', true, [model], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, false); + assert.strictEqual(result.model?.identifier, 'copilot/gpt'); + }); + + test('does NOT restore model that no longer exists', () => { + const otherModel = createModel('claude', 'Claude'); + const result = shouldRestorePersistedModel('copilot/gpt', false, [otherModel], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, false); + assert.strictEqual(result.model, undefined); + }); + + test('handles empty models list', () => { + const result = shouldRestorePersistedModel('copilot/gpt', false, [], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, false); + assert.strictEqual(result.model, undefined); + }); + + test('user choice is preserved when default changes to a different model', () => { + // User explicitly chose GPT-4o, default used to be Claude, now default is something else + const gpt = createModel('gpt-4o', 'GPT-4o'); + const claude = createModel('claude', 'Claude'); + const result = shouldRestorePersistedModel('copilot/gpt-4o', false, [gpt, claude], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, true); + assert.strictEqual(result.model?.metadata.id, 'gpt-4o'); + }); + + test('default tracking: follows new default when user never explicitly chose', () => { + // Old default was GPT-4o (persisted as default), now Claude is the default + const gpt = createModel('gpt-4o', 'GPT-4o'); + const claude = createDefaultModelForLocation('claude', 'Claude', ChatAgentLocation.Chat); + const result = shouldRestorePersistedModel('copilot/gpt-4o', true, [gpt, claude], ChatAgentLocation.Chat); + // Should NOT restore because GPT-4o is no longer default and was stored as default + assert.strictEqual(result.shouldRestore, false); + }); + }); + + suite('shouldResetModelToDefault', () => { + + const defaultContext = { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, + sessionType: undefined, + }; + + test('should reset when current model is undefined', () => { + assert.strictEqual(shouldResetModelToDefault(undefined, [], defaultContext, []), true); + }); + + test('should reset when model is no longer available', () => { + const model = createModel('gpt', 'GPT'); + assert.strictEqual(shouldResetModelToDefault(model, [], defaultContext, [model]), true); + }); + + test('should NOT reset when model is available and compatible', () => { + const model = createModel('gpt', 'GPT'); + assert.strictEqual(shouldResetModelToDefault(model, [model], defaultContext, [model]), false); + }); + + test('should reset when model is not supported for current mode', () => { + const model = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false, agentMode: false }, + }); + const context = { ...defaultContext, currentModeKind: ChatModeKind.Agent }; + assert.strictEqual(shouldResetModelToDefault(model, [model], context, [model]), true); + }); + + test('should reset when model is not supported for inline chat', () => { + const model = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false }, + }); + const context = { + ...defaultContext, + location: ChatAgentLocation.EditorInline, + isInlineChatV2Enabled: true, + }; + assert.strictEqual(shouldResetModelToDefault(model, [model], context, [model]), true); + }); + + test('should reset when model is not valid for session', () => { + const generalModel = createModel('gpt', 'GPT'); + const sessionModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const allModels = [generalModel, sessionModel]; + const context = { ...defaultContext, sessionType: 'cloud' }; + assert.strictEqual(shouldResetModelToDefault(generalModel, [generalModel], context, allModels), true); + }); + + test('should NOT reset session model in matching session', () => { + const sessionModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const context = { ...defaultContext, sessionType: 'cloud' }; + assert.strictEqual(shouldResetModelToDefault(sessionModel, [sessionModel], context, [sessionModel]), false); + }); + }); + + suite('resolveModelFromSyncState', () => { + + test('keeps current model when same as state model', () => { + const model = createModel('gpt', 'GPT'); + const result = resolveModelFromSyncState(model, model, [model], undefined); + assert.strictEqual(result.action, 'keep'); + }); + + test('applies state model when different and valid', () => { + const current = createModel('gpt', 'GPT'); + const stateModel = createModel('claude', 'Claude'); + const result = resolveModelFromSyncState(stateModel, current, [current, stateModel], undefined); + assert.strictEqual(result.action, 'apply'); + }); + + test('uses default when state model not valid for session', () => { + const current = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const stateModel = createModel('gpt', 'GPT'); // general model, not valid for cloud session + const allModels = [current, stateModel]; + const result = resolveModelFromSyncState(stateModel, current, allModels, 'cloud'); + assert.strictEqual(result.action, 'default'); + }); + + test('applies when current model is undefined', () => { + const stateModel = createModel('gpt', 'GPT'); + const result = resolveModelFromSyncState(stateModel, undefined, [stateModel], undefined); + assert.strictEqual(result.action, 'apply'); + }); + + test('applies session model when valid for matching session', () => { + const sessionModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, sessionModel]; + const result = resolveModelFromSyncState(sessionModel, generalModel, allModels, 'cloud'); + assert.strictEqual(result.action, 'apply'); + }); + + test('returns default when state model does not support current mode', () => { + const current = createModel('gpt', 'GPT'); + const stateModel = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false, agentMode: false }, + }); + const result = resolveModelFromSyncState(stateModel, current, [current, stateModel], undefined, { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: undefined, + }); + assert.strictEqual(result.action, 'default'); + }); + + test('returns default when state model does not support inline chat V2', () => { + const current = createModel('gpt', 'GPT'); + const stateModel = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false }, + }); + const result = resolveModelFromSyncState(stateModel, current, [current, stateModel], undefined, { + location: ChatAgentLocation.EditorInline, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: true, + sessionType: undefined, + }); + assert.strictEqual(result.action, 'default'); + }); + + test('applies when state model supports current mode with context', () => { + const current = createModel('gpt', 'GPT'); + const stateModel = createModel('agent-model', 'Agent Model', { + capabilities: { toolCalling: true, agentMode: true }, + }); + const result = resolveModelFromSyncState(stateModel, current, [current, stateModel], undefined, { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: undefined, + }); + assert.strictEqual(result.action, 'apply'); + }); + }); + + suite('mergeModelsWithCache', () => { + + test('uses live models when available', () => { + const liveModel = createModel('gpt', 'GPT'); + const cachedModel = createModel('cached-gpt', 'Cached GPT'); + const result = mergeModelsWithCache([liveModel], [cachedModel], new Set(['copilot'])); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].metadata.id, 'gpt'); + }); + + test('falls back to cached models when no live models', () => { + const cachedModel = createModel('cached-gpt', 'Cached GPT'); + const result = mergeModelsWithCache([], [cachedModel], new Set(['copilot'])); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].metadata.id, 'cached-gpt'); + }); + + test('merges cached models from vendors not yet resolved', () => { + const liveModel = createModel('gpt', 'GPT'); + const cachedOtherVendor = createModel('other-model', 'Other Model', { vendor: 'other-vendor' }); + const result = mergeModelsWithCache( + [liveModel], + [cachedOtherVendor], + new Set(['copilot', 'other-vendor']), + ); + assert.strictEqual(result.length, 2); + assert.deepStrictEqual(result.map(m => m.metadata.id).sort(), ['gpt', 'other-model']); + }); + + test('evicts cached models from vendors no longer contributed', () => { + const liveModel = createModel('gpt', 'GPT'); + const cachedRemovedVendor = createModel('removed-model', 'Removed Model', { vendor: 'removed-vendor' }); + const result = mergeModelsWithCache( + [liveModel], + [cachedRemovedVendor], + new Set(['copilot']), // removed-vendor is NOT contributed + ); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].metadata.id, 'gpt'); + }); + + test('does not duplicate models from same vendor', () => { + const liveModel = createModel('gpt', 'GPT'); + const cachedSameVendor = createModel('cached-gpt', 'Cached GPT'); + const result = mergeModelsWithCache( + [liveModel], + [cachedSameVendor], + new Set(['copilot']), + ); + // Both are vendor 'copilot', live vendor takes priority + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].metadata.id, 'gpt'); + }); + + test('handles empty cache and empty live models', () => { + const result = mergeModelsWithCache([], [], new Set()); + assert.deepStrictEqual(result, []); + }); + + test('handles multiple vendors with partial resolution', () => { + const liveA = createModel('a-model', 'A Model', { vendor: 'vendor-a' }); + const cachedB = createModel('b-model', 'B Model', { vendor: 'vendor-b' }); + const cachedC = createModel('c-model', 'C Model', { vendor: 'vendor-c' }); + const result = mergeModelsWithCache( + [liveA], + [cachedB, cachedC], + new Set(['vendor-a', 'vendor-b']), // vendor-c not contributed + ); + assert.strictEqual(result.length, 2); + assert.deepStrictEqual(result.map(m => m.metadata.vendor).sort(), ['vendor-a', 'vendor-b']); + }); + }); + + suite('model switching scenarios', () => { + + test('switching from Ask to Agent mode should reset model without tool support', () => { + const noToolsModel = createModel('no-tools', 'No-Tools', { + capabilities: { toolCalling: false, agentMode: false }, + }); + const toolModel = createModel('tool-model', 'Tool Model'); + const allModels = [noToolsModel, toolModel]; + + // In Ask mode, model is fine + assert.strictEqual( + shouldResetModelToDefault(noToolsModel, allModels, { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, + sessionType: undefined, + }, allModels), + false, + ); + + // After switching to Agent mode, model should be reset + assert.strictEqual( + shouldResetModelToDefault(noToolsModel, allModels, { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: undefined, + }, allModels), + true, + ); + }); + + test('switching sessions should reject model from wrong session pool', () => { + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, cloudModel]; + + // Cloud model is valid in cloud session + assert.strictEqual( + isModelValidForSession(cloudModel, allModels, 'cloud'), + true, + ); + + // Cloud model is NOT valid in general session (no session type) + assert.strictEqual( + isModelValidForSession(cloudModel, allModels, undefined), + false, + ); + + // General model is NOT valid in cloud session (when cloud models exist) + assert.strictEqual( + isModelValidForSession(generalModel, allModels, 'cloud'), + false, + ); + + // General model IS valid in general session + assert.strictEqual( + isModelValidForSession(generalModel, allModels, undefined), + true, + ); + }); + + test('model removal should trigger reset', () => { + const gpt = createModel('gpt', 'GPT'); + const claude = createModel('claude', 'Claude'); + + // Initially both available, GPT is selected + assert.strictEqual( + shouldResetModelToDefault(gpt, [gpt, claude], { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, + sessionType: undefined, + }, [gpt, claude]), + false, + ); + + // GPT is removed from available models + assert.strictEqual( + shouldResetModelToDefault(gpt, [claude], { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, + sessionType: undefined, + }, [claude]), + true, + ); + }); + + test('syncing model from state respects session boundaries', () => { + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, cloudModel]; + + // State has a cloud model, but we are in a general session + const result = resolveModelFromSyncState(cloudModel, generalModel, allModels, undefined); + assert.strictEqual(result.action, 'default'); + }); + + test('syncing model from state applies model when switching to matching session', () => { + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, cloudModel]; + + // State has a cloud model and we are in a cloud session + const result = resolveModelFromSyncState(cloudModel, generalModel, allModels, 'cloud'); + assert.strictEqual(result.action, 'apply'); + }); + + test('persisted model selection survives when model is still default', () => { + const model = createDefaultModelForLocation('gpt-4o', 'GPT-4o', ChatAgentLocation.Chat); + const result = shouldRestorePersistedModel('copilot/gpt-4o', true, [model], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, true); + }); + + test('persisted model selection does NOT restore when a new default is assigned', () => { + // GPT-4o was the old default (persisted as default=true), but it's no longer default + const gpt4o = createModel('gpt-4o', 'GPT-4o'); + const newDefault = createDefaultModelForLocation('claude', 'Claude', ChatAgentLocation.Chat); + const result = shouldRestorePersistedModel('copilot/gpt-4o', true, [gpt4o, newDefault], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, false); + }); + + test('user explicit model choice persists even when default changes', () => { + // User explicitly picked Claude (persistedAsDefault=false), default was GPT-4o + // Now default switches to something else — Claude should still be restored + const claude = createModel('claude', 'Claude'); + const newDefault = createDefaultModelForLocation('new-model', 'New Model', ChatAgentLocation.Chat); + const result = shouldRestorePersistedModel('copilot/claude', false, [claude, newDefault], ChatAgentLocation.Chat); + assert.strictEqual(result.shouldRestore, true); + assert.strictEqual(result.model?.metadata.id, 'claude'); + }); + + test('combining mode switch + session switch validates correctly', () => { + const cloudToolModel = createSessionModel('cloud-tool', 'Cloud Tool', 'cloud', { + capabilities: { toolCalling: true, agentMode: true }, + }); + const cloudNoToolModel = createSessionModel('cloud-basic', 'Cloud Basic', 'cloud', { + capabilities: { toolCalling: false, agentMode: false }, + }); + const allCloudModels = [cloudToolModel, cloudNoToolModel]; + + // In cloud session, Agent mode — tool model is valid + assert.strictEqual( + shouldResetModelToDefault(cloudToolModel, allCloudModels, { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: 'cloud', + }, allCloudModels), + false, + ); + + // The no-tool model should be reset in Agent mode + // Both filterModelsForSession and shouldResetModelToDefault enforce mode support + assert.strictEqual( + shouldResetModelToDefault(cloudNoToolModel, allCloudModels, { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: 'cloud', + }, allCloudModels), + true, + ); + }); + }); + + suite('onDidChangeLanguageModels race conditions', () => { + + test('model temporarily removed then re-added loses user choice', () => { + const gpt = createModel('gpt', 'GPT'); + const claude = createModel('claude', 'Claude'); + + // Step 1: User has GPT selected, both models available + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', [gpt, claude]), false); + + // Step 2: Extension reloads, GPT temporarily disappears from model list + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', [claude]), true); + // → ChatInputPart resets to default (Claude) + + // Step 3: GPT comes back — but the handler just checks if current is still valid. + // By now the current is Claude (from step 2), so it stays. + assert.strictEqual(shouldResetOnModelListChange('copilot/claude', [gpt, claude]), false); + // → User's original GPT choice is lost! This is the "random switch" bug pattern. + }); + + test('model stays when model list refreshes with it still present', () => { + const gpt = createModel('gpt', 'GPT'); + const claude = createModel('claude', 'Claude'); + + // Model list refreshes but GPT is still there + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', [gpt, claude]), false); + }); + + test('reset when current model identifier is undefined', () => { + const gpt = createModel('gpt', 'GPT'); + assert.strictEqual(shouldResetOnModelListChange(undefined, [gpt]), true); + }); + + test('reset when models list is empty', () => { + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', []), true); + }); + + test('cache bridges the gap when live models temporarily unavailable', () => { + const cachedGpt = createModel('gpt', 'GPT'); + const cachedClaude = createModel('claude', 'Claude'); + + // Step 1: Extension unloaded, no live models. Cache fills the gap. + const merged = mergeModelsWithCache([], [cachedGpt, cachedClaude], new Set(['copilot'])); + assert.strictEqual(merged.length, 2); + + // Selected model is still found in the cached list + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', merged), false); + }); + + test('cache kept even for uncontributed vendors when no live models exist', () => { + const cachedGpt = createModel('gpt', 'GPT'); + + // When liveModels is empty, mergeModelsWithCache returns ALL cached + // because it can't distinguish "startup not ready" from "vendor removed" + const merged = mergeModelsWithCache([], [cachedGpt], new Set()); + assert.strictEqual(merged.length, 1); + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', merged), false); + }); + + test('cache evicted for uncontributed vendor once live models arrive', () => { + const cachedGpt = createModel('gpt', 'GPT'); + const liveOther = createModel('other', 'Other', { vendor: 'other-vendor' }); + + // Once live models exist, the vendor filter kicks in + const merged = mergeModelsWithCache([liveOther], [cachedGpt], new Set(['other-vendor'])); + assert.strictEqual(merged.length, 1); + assert.strictEqual(merged[0].metadata.id, 'other'); + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', merged), true); + }); + }); + + suite('late-arriving model restoration', () => { + + test('restores explicitly-chosen model that arrives late', () => { + const model = createModel('gpt', 'GPT'); + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/gpt', false, model, ChatAgentLocation.Chat), + true, + ); + }); + + test('restores model that was default and is still default for location', () => { + const model = createDefaultModelForLocation('gpt', 'GPT', ChatAgentLocation.Chat); + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/gpt', true, model, ChatAgentLocation.Chat), + true, + ); + }); + + test('does NOT restore model that was default but is no longer default', () => { + const model = createModel('gpt', 'GPT'); // not default for any location + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/gpt', true, model, ChatAgentLocation.Chat), + false, + ); + }); + + test('does NOT restore model that is not user-selectable', () => { + const model = createModel('internal', 'Internal', { isUserSelectable: false }); + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/internal', false, model, ChatAgentLocation.Chat), + false, + ); + }); + + test('does NOT restore model with isUserSelectable=undefined (treated as falsy)', () => { + const model = createModel('undef-sel', 'Undef-Sel', { isUserSelectable: undefined }); + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/undef-sel', false, model, ChatAgentLocation.Chat), + false, + ); + }); + + test('restores model arriving late at a different location where it is default', () => { + const model = createDefaultModelForLocation('gpt', 'GPT', ChatAgentLocation.Terminal); + // User is in Terminal — model is default there + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/gpt', true, model, ChatAgentLocation.Terminal), + true, + ); + // But not in Chat + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/gpt', true, model, ChatAgentLocation.Chat), + false, + ); + }); + }); + + suite('full startup pipeline (computeAvailableModels)', () => { + + test('startup with only cached models returns filtered cache', () => { + const cached = createModel('gpt', 'GPT'); + const result = computeAvailableModels( + [], // no live models yet + [cached], + new Set(['copilot']), + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt']); + }); + + test('startup with cached models from removed vendor still returns them (no live to compare)', () => { + const cached = createModel('gpt', 'GPT'); + // When liveModels is empty, mergeModelsWithCache returns ALL cached + // because it cannot tell startup-delay from vendor removal + const result = computeAvailableModels( + [], // no live models + [cached], + new Set(), // vendor no longer contributed + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt']); + }); + + test('live models supersede cached models from same vendor', () => { + const live = createModel('gpt-new', 'GPT New'); + const cached = createModel('gpt-old', 'GPT Old'); + const result = computeAvailableModels( + [live], + [cached], + new Set(['copilot']), + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt-new']); + }); + + test('partial vendor resolution keeps unresolved vendors from cache', () => { + const liveA = createModel('a-model', 'A Model', { vendor: 'vendor-a' }); + const cachedB = createModel('b-model', 'B Model', { vendor: 'vendor-b' }); + const result = computeAvailableModels( + [liveA], + [cachedB], + new Set(['vendor-a', 'vendor-b']), + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id).sort(), ['a-model', 'b-model']); + }); + + test('results are sorted alphabetically by name', () => { + const modelC = createModel('c', 'Charlie'); + const modelA = createModel('a', 'Alpha'); + const modelB = createModel('b', 'Bravo'); + const result = computeAvailableModels( + [modelC, modelA, modelB], + [], + new Set(['copilot']), + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.name), ['Alpha', 'Bravo', 'Charlie']); + }); + + test('session-targeted models excluded from general session startup', () => { + const general = createModel('gpt', 'GPT'); + const cloudOnly = createSessionModel('cloud', 'Cloud', 'cloud'); + const result = computeAvailableModels( + [general, cloudOnly], + [], + new Set(['copilot']), + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['gpt']); + }); + + test('only session-targeted models returned for cloud session startup', () => { + const general = createModel('gpt', 'GPT'); + const cloudOnly = createSessionModel('cloud', 'Cloud', 'cloud'); + const result = computeAvailableModels( + [general, cloudOnly], + [], + new Set(['copilot']), + 'cloud', + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['cloud']); + }); + + test('agent mode filters non-tool models during startup', () => { + const toolModel = createModel('tool', 'Tool Model'); + const noToolModel = createModel('no-tool', 'No Tool', { + capabilities: { toolCalling: false, agentMode: false }, + }); + const result = computeAvailableModels( + [toolModel, noToolModel], + [], + new Set(['copilot']), + undefined, + ChatModeKind.Agent, + ChatAgentLocation.Chat, + false, + ); + assert.deepStrictEqual(result.map(m => m.metadata.id), ['tool']); + }); + }); + + suite('_syncFromModel edge cases', () => { + + test('sync state with undefined selectedModel keeps current', () => { + const current = createModel('gpt', 'GPT'); + // When state has no selectedModel, _syncFromModel skips the model sync + // (the code checks `if (state?.selectedModel)`) + // This means the current model stays — test that resolveModelFromSyncState + // correctly identifies "keep" for same model + const result = resolveModelFromSyncState(current, current, [current], undefined); + assert.strictEqual(result.action, 'keep'); + }); + + test('sync state model from different session does not apply', () => { + // Scenario: User is in session A with cloud model, switches to session B (general) + // Session B's state still has the cloud model reference + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, cloudModel]; + + const result = resolveModelFromSyncState(cloudModel, generalModel, allModels, undefined); + assert.strictEqual(result.action, 'default'); + }); + + test('sync state with model matching different session type falls back to default', () => { + const enterpriseModel = createSessionModel('ent-gpt', 'Enterprise GPT', 'enterprise'); + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const allModels = [cloudModel, enterpriseModel]; + + // State has enterprise model, but we're in cloud session + const result = resolveModelFromSyncState(enterpriseModel, cloudModel, allModels, 'cloud'); + assert.strictEqual(result.action, 'default'); + }); + + test('sync identical model reference returns keep', () => { + const model = createModel('gpt', 'GPT'); + // Same object reference + const result = resolveModelFromSyncState(model, model, [model], undefined); + assert.strictEqual(result.action, 'keep'); + }); + + test('sync same identifier but different object returns keep', () => { + const model1 = createModel('gpt', 'GPT'); + const model2 = createModel('gpt', 'GPT'); + // Different objects, same identifier + const result = resolveModelFromSyncState(model1, model2, [model1, model2], undefined); + assert.strictEqual(result.action, 'keep'); + }); + }); + + suite('checkModelSupported interaction patterns', () => { + + const askContext = { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, + sessionType: undefined, + }; + + const agentContext = { + ...askContext, + currentModeKind: ChatModeKind.Agent, + }; + + test('initSelectedModel → checkModelSupported: restored model passes Agent check', () => { + const agentModel = createModel('agent-model', 'Agent Model', { + capabilities: { toolCalling: true, agentMode: true }, + }); + + // 1. shouldRestorePersistedModel says "restore" + const restoreResult = shouldRestorePersistedModel('copilot/agent-model', false, [agentModel], ChatAgentLocation.Chat); + assert.strictEqual(restoreResult.shouldRestore, true); + + // 2. Immediately after, checkModelSupported runs with Agent mode + assert.strictEqual(shouldResetModelToDefault(agentModel, [agentModel], agentContext, [agentModel]), false); + }); + + test('initSelectedModel → checkModelSupported: restored model FAILS Agent check', () => { + const askOnlyModel = createModel('ask-only', 'Ask Only', { + capabilities: { toolCalling: false, agentMode: false }, + }); + const agentModel = createModel('agent-model', 'Agent Model'); + + // 1. shouldRestorePersistedModel says "restore" + const restoreResult = shouldRestorePersistedModel('copilot/ask-only', false, [askOnlyModel, agentModel], ChatAgentLocation.Chat); + assert.strictEqual(restoreResult.shouldRestore, true); + + // 2. checkModelSupported runs with Agent mode → should reset + assert.strictEqual(shouldResetModelToDefault(askOnlyModel, [askOnlyModel, agentModel], agentContext, [askOnlyModel, agentModel]), true); + + // 3. findDefaultModel picks replacement from models filtered for Agent mode + const agentCompatibleModels = filterModelsForSession( + [askOnlyModel, agentModel], undefined, ChatModeKind.Agent, ChatAgentLocation.Chat, false + ); + const defaultModel = findDefaultModel(agentCompatibleModels, ChatAgentLocation.Chat); + assert.strictEqual(defaultModel?.metadata.id, 'agent-model'); + }); + + test('mode switch triggers checkModelSupported which resets incompatible model', () => { + const noToolModel = createModel('no-tool', 'No Tool', { + capabilities: { toolCalling: false }, + }); + const toolModel = createModel('tool', 'Tool'); + + // In Ask mode: fine + assert.strictEqual(shouldResetModelToDefault(noToolModel, [noToolModel, toolModel], askContext, [noToolModel, toolModel]), false); + + // Switch to Agent mode: not fine + assert.strictEqual(shouldResetModelToDefault(noToolModel, [noToolModel, toolModel], agentContext, [noToolModel, toolModel]), true); + }); + + test('double reset is idempotent', () => { + const defaultModel = createDefaultModelForLocation('default', 'Default', ChatAgentLocation.Chat); + const otherModel = createModel('other', 'Other'); + const allModels = [defaultModel, otherModel]; + + // First reset: picks default + const result1 = findDefaultModel(allModels, ChatAgentLocation.Chat); + assert.strictEqual(result1?.metadata.id, 'default'); + + // "Second reset" — same call, same result + const result2 = findDefaultModel(allModels, ChatAgentLocation.Chat); + assert.strictEqual(result2?.metadata.id, 'default'); + + // Default model continues to pass validation + assert.strictEqual(shouldResetModelToDefault(result1!, allModels, askContext, allModels), false); + }); + }); + + suite('multiple session types and cross-contamination', () => { + + test('model from session A rejected in session B', () => { + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const enterpriseModel = createSessionModel('ent-gpt', 'Enterprise GPT', 'enterprise'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, cloudModel, enterpriseModel]; + + // Cloud model not valid in enterprise session + assert.strictEqual(isModelValidForSession(cloudModel, allModels, 'enterprise'), false); + // Enterprise model not valid in cloud session + assert.strictEqual(isModelValidForSession(enterpriseModel, allModels, 'cloud'), false); + // General model not valid when session-targeted models exist + assert.strictEqual(isModelValidForSession(generalModel, allModels, 'cloud'), false); + }); + + test('general model is valid when session type has no targeted models', () => { + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, cloudModel]; + + // 'enterprise' session has no targeted models + assert.strictEqual(isModelValidForSession(generalModel, allModels, 'enterprise'), true); + }); + + test('filterModelsForSession isolates session types correctly', () => { + const general = createModel('gpt', 'GPT'); + const cloud = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const enterprise = createSessionModel('ent-gpt', 'Enterprise GPT', 'enterprise'); + const allModels = [general, cloud, enterprise]; + + const cloudFiltered = filterModelsForSession(allModels, 'cloud', ChatModeKind.Ask, ChatAgentLocation.Chat, false); + assert.deepStrictEqual(cloudFiltered.map(m => m.metadata.id), ['cloud-gpt']); + + const entFiltered = filterModelsForSession(allModels, 'enterprise', ChatModeKind.Ask, ChatAgentLocation.Chat, false); + assert.deepStrictEqual(entFiltered.map(m => m.metadata.id), ['ent-gpt']); + + const generalFiltered = filterModelsForSession(allModels, undefined, ChatModeKind.Ask, ChatAgentLocation.Chat, false); + assert.deepStrictEqual(generalFiltered.map(m => m.metadata.id), ['gpt']); + }); + + test('switching from cloud to general session resets cloud model', () => { + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud'); + const generalModel = createModel('gpt', 'GPT'); + const allModels = [generalModel, cloudModel]; + + // In cloud session, cloud model is valid + assert.strictEqual(shouldResetModelToDefault(cloudModel, [cloudModel], { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, + sessionType: 'cloud', + }, allModels), false); + + // Switch to general session — cloud model should be reset + assert.strictEqual(shouldResetModelToDefault(cloudModel, [generalModel], { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, + sessionType: undefined, + }, allModels), true); + }); + }); + + suite('mode with forced model (mode.model property)', () => { + + test('mode forces model — simulating switchModelByQualifiedName success', () => { + const gpt = createModel('gpt-4o', 'GPT-4o'); + const claude = createModel('claude', 'Claude'); + const allModels = [gpt, claude]; + + // The autorun calls switchModelByQualifiedName which checks ILanguageModelChatMetadata.matchesQualifiedName + // Simulate: mode wants "GPT-4o (copilot)" + const qualifiedName = 'GPT-4o (copilot)'; + const match = allModels.find(m => ILanguageModelChatMetadata.matchesQualifiedName(qualifiedName, m.metadata)); + assert.strictEqual(match?.metadata.id, 'gpt-4o'); + }); + + test('mode forces model — copilot vendor shorthand works', () => { + const gpt = createModel('gpt-4o', 'GPT-4o'); + // For copilot vendor, just the name works + const match = [gpt].find(m => ILanguageModelChatMetadata.matchesQualifiedName('GPT-4o', m.metadata)); + assert.strictEqual(match?.metadata.id, 'gpt-4o'); + }); + + test('mode forces model — nonexistent model gracefully misses', () => { + const gpt = createModel('gpt-4o', 'GPT-4o'); + const match = [gpt].find(m => ILanguageModelChatMetadata.matchesQualifiedName('NonExistent (copilot)', m.metadata)); + assert.strictEqual(match, undefined); + }); + + test('mode forces model that is then checked for support', () => { + // Mode forces a model, then checkModelSupported runs + const forcedModel = createModel('forced', 'Forced', { + capabilities: { toolCalling: false, agentMode: false }, + }); + + // Mode forced this model but we're in Agent mode — should be reset + assert.strictEqual(shouldResetModelToDefault(forcedModel, [forcedModel], { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: undefined, + }, [forcedModel]), true); + }); + }); + + suite('EditorInline + mode combined scenarios', () => { + + test('EditorInline + Agent + V2 requires both agentMode and toolCalling', () => { + const partialModel = createModel('partial', 'Partial', { + capabilities: { toolCalling: true, agentMode: false }, + }); + // Fails Agent mode check + assert.strictEqual(isModelSupportedForMode(partialModel, ChatModeKind.Agent), false); + // Passes inline chat check (has toolCalling) + assert.strictEqual(isModelSupportedForInlineChat(partialModel, ChatAgentLocation.EditorInline, true), true); + + // Combined: should reset because Agent mode fails + assert.strictEqual(shouldResetModelToDefault(partialModel, [partialModel], { + location: ChatAgentLocation.EditorInline, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: true, + sessionType: undefined, + }, [partialModel]), true); + }); + + test('EditorInline + Ask + V2 only requires toolCalling', () => { + const toolModel = createModel('tool', 'Tool'); + assert.strictEqual(shouldResetModelToDefault(toolModel, [toolModel], { + location: ChatAgentLocation.EditorInline, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: true, + sessionType: undefined, + }, [toolModel]), false); + }); + + test('EditorInline + Ask + V2 rejects model without toolCalling', () => { + const noToolModel = createModel('no-tool', 'No Tool', { + capabilities: {}, + }); + assert.strictEqual(shouldResetModelToDefault(noToolModel, [noToolModel], { + location: ChatAgentLocation.EditorInline, + currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: true, + sessionType: undefined, + }, [noToolModel]), true); + }); + }); + + suite('findDefaultModel edge cases', () => { + + test('when all models are session-targeted and none is default, first model wins', () => { + const m1 = createSessionModel('s1', 'Session 1', 'cloud'); + const m2 = createSessionModel('s2', 'Session 2', 'cloud'); + const result = findDefaultModel([m1, m2], ChatAgentLocation.Chat); + assert.strictEqual(result?.metadata.id, 's1'); + }); + + test('default for one location does not leak to another', () => { + const chatDefault = createDefaultModelForLocation('chat-def', 'Chat Default', ChatAgentLocation.Chat); + const noDefault = createModel('no-def', 'No Default'); + + // For Chat: chatDefault wins + assert.strictEqual(findDefaultModel([noDefault, chatDefault], ChatAgentLocation.Chat)?.metadata.id, 'chat-def'); + // For Terminal: no model is default, so first model wins + assert.strictEqual(findDefaultModel([noDefault, chatDefault], ChatAgentLocation.Terminal)?.metadata.id, 'no-def'); + }); + }); + + suite('realistic multi-step race simulations', () => { + + test('startup: cached model → live models arrive → user choice preserved', () => { + const cachedGpt = createModel('gpt', 'GPT'); + const cachedClaude = createModel('claude', 'Claude'); + + // Step 1: Startup with only cache. User had GPT selected. + const cachedModels = computeAvailableModels( + [], + [cachedGpt, cachedClaude], + new Set(['copilot']), + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + // GPT is in the cached list + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', cachedModels), false); + + // Step 2: Live models arrive (same models) + const liveModels = computeAvailableModels( + [cachedGpt, cachedClaude], + [cachedGpt, cachedClaude], + new Set(['copilot']), + undefined, + ChatModeKind.Ask, + ChatAgentLocation.Chat, + false, + ); + // GPT still in the list — no reset needed + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', liveModels), false); + }); + + test('startup: no cache → models arrive late → persisted choice restored', () => { + // Step 1: No models available at all + const emptyModels = computeAvailableModels([], [], new Set(['copilot']), undefined, ChatModeKind.Ask, ChatAgentLocation.Chat, false); + assert.strictEqual(emptyModels.length, 0); + + // initSelectedModel: model not found, enters _waitForPersistedLanguageModel path + const restoreResult = shouldRestorePersistedModel('copilot/gpt', false, emptyModels, ChatAgentLocation.Chat); + assert.strictEqual(restoreResult.shouldRestore, false); + assert.strictEqual(restoreResult.model, undefined); + + // Step 2: Models arrive via onDidChangeLanguageModels + const arrivedModel = createModel('gpt', 'GPT'); + assert.strictEqual( + shouldRestoreLateArrivingModel('copilot/gpt', false, arrivedModel, ChatAgentLocation.Chat), + true, + ); + }); + + test('extension reload: selected model flickers out then back', () => { + const gpt = createModel('gpt', 'GPT'); + const claude = createModel('claude', 'Claude'); + + // Step 1: GPT is selected + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', [gpt, claude]), false); + + // Step 2: Extension reloads, copilot vendor has no live models + // But cache bridges the gap + const duringReload = mergeModelsWithCache([], [gpt, claude], new Set(['copilot'])); + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', duringReload), false); + + // Step 3: Extension finishes loading, live models back + const afterReload = mergeModelsWithCache([gpt, claude], [gpt, claude], new Set(['copilot'])); + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', afterReload), false); + }); + + test('extension reload without cache: model lost', () => { + const gpt = createModel('gpt', 'GPT'); + + // Step 1: GPT selected, no cache + // Step 2: Extension reloads with no models and no cache + const duringReload = mergeModelsWithCache([], [], new Set(['copilot'])); + assert.strictEqual(duringReload.length, 0); + assert.strictEqual(shouldResetOnModelListChange('copilot/gpt', duringReload), true); + // → Model is lost, reset to default + + // Step 3: Models come back but user's choice is already gone + const afterReload = mergeModelsWithCache([gpt], [], new Set(['copilot'])); + assert.strictEqual(afterReload.length, 1); + // User's selection was already reset to something else + // This is expected behavior — cache is the mitigation + }); + + test('session switch race: mode + session change together', () => { + const generalDefault = createDefaultModelForLocation('gpt', 'GPT', ChatAgentLocation.Chat); + const cloudModel = createSessionModel('cloud-gpt', 'Cloud GPT', 'cloud', { + capabilities: { toolCalling: true, agentMode: true }, + }); + const allModels = [generalDefault, cloudModel]; + + // User is in general session with GPT in Agent mode + assert.strictEqual(shouldResetModelToDefault(generalDefault, [generalDefault], { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: undefined, + }, allModels), false); + + // Switch to cloud session — general model should be reset + assert.strictEqual(shouldResetModelToDefault(generalDefault, [cloudModel], { + location: ChatAgentLocation.Chat, + currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, + sessionType: 'cloud', + }, allModels), true); + + // The default for cloud session should be the cloud model + const cloudDefault = findDefaultModel([cloudModel], ChatAgentLocation.Chat); + assert.strictEqual(cloudDefault?.metadata.id, 'cloud-gpt'); + }); + + test('rapid mode changes: ask → agent → ask preserves compatible model', () => { + const model = createModel('gpt', 'GPT'); // Compatible with all modes + const allModels = [model]; + + // Ask mode: fine + assert.strictEqual(shouldResetModelToDefault(model, allModels, { + location: ChatAgentLocation.Chat, currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, sessionType: undefined, + }, allModels), false); + + // → Agent mode: model has toolCalling, still fine + assert.strictEqual(shouldResetModelToDefault(model, allModels, { + location: ChatAgentLocation.Chat, currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, sessionType: undefined, + }, allModels), false); + + // → Back to Ask: still fine + assert.strictEqual(shouldResetModelToDefault(model, allModels, { + location: ChatAgentLocation.Chat, currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, sessionType: undefined, + }, allModels), false); + }); + + test('rapid mode changes: ask → agent resets incompatible, then agent → ask does not restore', () => { + const noToolModel = createModel('no-tool', 'No Tool', { + capabilities: { toolCalling: false }, + }); + const toolModel = createDefaultModelForLocation('tool', 'Tool', ChatAgentLocation.Chat); + const allModels = [noToolModel, toolModel]; + + // Ask mode with noToolModel: fine + assert.strictEqual(shouldResetModelToDefault(noToolModel, allModels, { + location: ChatAgentLocation.Chat, currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, sessionType: undefined, + }, allModels), false); + + // → Agent mode: noToolModel fails, reset picks default (toolModel) + assert.strictEqual(shouldResetModelToDefault(noToolModel, allModels, { + location: ChatAgentLocation.Chat, currentModeKind: ChatModeKind.Agent, + isInlineChatV2Enabled: false, sessionType: undefined, + }, allModels), true); + const defaultAfterReset = findDefaultModel(allModels, ChatAgentLocation.Chat); + assert.strictEqual(defaultAfterReset?.metadata.id, 'tool'); + + // → Back to Ask: toolModel is fine in Ask mode, stays as toolModel + // The original noToolModel is NOT restored — this is expected and matches ChatInputPart behavior + assert.strictEqual(shouldResetModelToDefault(toolModel, allModels, { + location: ChatAgentLocation.Chat, currentModeKind: ChatModeKind.Ask, + isInlineChatV2Enabled: false, sessionType: undefined, + }, allModels), false); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts index 56cf17fafc88f..ee30e3f60dc08 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/hookSchema.test.ts @@ -5,9 +5,11 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { resolveHookCommand, resolveEffectiveCommand, formatHookCommandLabel, IHookCommand } from '../../../common/promptSyntax/hookSchema.js'; +import { resolveHookCommand, resolveEffectiveCommand, formatHookCommandLabel, IHookCommand, parseSubagentHooksFromYaml } from '../../../common/promptSyntax/hookSchema.js'; import { URI } from '../../../../../../base/common/uri.js'; import { OperatingSystem } from '../../../../../../base/common/platform.js'; +import { HookType } from '../../../common/promptSyntax/hookTypes.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; suite('HookSchema', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -485,4 +487,162 @@ suite('HookSchema', () => { assert.strictEqual(formatHookCommandLabel(hook, OperatingSystem.Windows), 'default-command'); }); }); + + suite('parseSubagentHooksFromYaml', () => { + + const workspaceRoot = URI.file('/workspace'); + const userHome = '/home/user'; + + const dummyRange = new Range(1, 1, 1, 1); + + function makeScalar(value: string): import('../../../common/promptSyntax/promptFileParser.js').IScalarValue { + return { type: 'scalar', value, range: dummyRange, format: 'none' }; + } + + function makeMap(entries: Record): import('../../../common/promptSyntax/promptFileParser.js').IMapValue { + const properties = Object.entries(entries).map(([key, value]) => ({ + key: makeScalar(key), + value, + })); + return { type: 'map', properties, range: dummyRange }; + } + + function makeSequence(items: import('../../../common/promptSyntax/promptFileParser.js').IValue[]): import('../../../common/promptSyntax/promptFileParser.js').ISequenceValue { + return { type: 'sequence', items, range: dummyRange }; + } + + test('parses direct command format (without matcher)', () => { + // hooks: + // PreToolUse: + // - type: command + // command: "./scripts/validate.sh" + const hooksMap = makeMap({ + 'PreToolUse': makeSequence([ + makeMap({ + 'type': makeScalar('command'), + 'command': makeScalar('./scripts/validate.sh'), + }), + ]), + }); + + const result = parseSubagentHooksFromYaml(hooksMap, workspaceRoot, userHome); + + assert.strictEqual(result[HookType.PreToolUse]?.length, 1); + assert.strictEqual(result[HookType.PreToolUse]![0].command, './scripts/validate.sh'); + }); + + test('parses Claude format (with matcher)', () => { + // hooks: + // PreToolUse: + // - matcher: "Bash" + // hooks: + // - type: command + // command: "./scripts/validate-readonly.sh" + const hooksMap = makeMap({ + 'PreToolUse': makeSequence([ + makeMap({ + 'matcher': makeScalar('Bash'), + 'hooks': makeSequence([ + makeMap({ + 'type': makeScalar('command'), + 'command': makeScalar('./scripts/validate-readonly.sh'), + }), + ]), + }), + ]), + }); + + const result = parseSubagentHooksFromYaml(hooksMap, workspaceRoot, userHome); + + assert.strictEqual(result[HookType.PreToolUse]?.length, 1); + assert.strictEqual(result[HookType.PreToolUse]![0].command, './scripts/validate-readonly.sh'); + }); + + test('parses multiple hook types', () => { + const hooksMap = makeMap({ + 'PreToolUse': makeSequence([ + makeMap({ + 'type': makeScalar('command'), + 'command': makeScalar('./scripts/pre.sh'), + }), + ]), + 'PostToolUse': makeSequence([ + makeMap({ + 'matcher': makeScalar('Edit|Write'), + 'hooks': makeSequence([ + makeMap({ + 'type': makeScalar('command'), + 'command': makeScalar('./scripts/lint.sh'), + }), + ]), + }), + ]), + }); + + const result = parseSubagentHooksFromYaml(hooksMap, workspaceRoot, userHome); + + assert.strictEqual(result[HookType.PreToolUse]?.length, 1); + assert.strictEqual(result[HookType.PreToolUse]![0].command, './scripts/pre.sh'); + assert.strictEqual(result[HookType.PostToolUse]?.length, 1); + assert.strictEqual(result[HookType.PostToolUse]![0].command, './scripts/lint.sh'); + }); + + test('skips unknown hook types', () => { + const hooksMap = makeMap({ + 'UnknownHook': makeSequence([ + makeMap({ + 'type': makeScalar('command'), + 'command': makeScalar('echo "ignored"'), + }), + ]), + }); + + const result = parseSubagentHooksFromYaml(hooksMap, workspaceRoot, userHome); + + assert.strictEqual(result[HookType.PreToolUse], undefined); + assert.strictEqual(result[HookType.PostToolUse], undefined); + }); + + test('handles command without type field', () => { + const hooksMap = makeMap({ + 'PreToolUse': makeSequence([ + makeMap({ + 'command': makeScalar('./scripts/validate.sh'), + }), + ]), + }); + + const result = parseSubagentHooksFromYaml(hooksMap, workspaceRoot, userHome); + + assert.strictEqual(result[HookType.PreToolUse]?.length, 1); + assert.strictEqual(result[HookType.PreToolUse]![0].command, './scripts/validate.sh'); + }); + + test('resolves cwd relative to workspace', () => { + const hooksMap = makeMap({ + 'SessionStart': makeSequence([ + makeMap({ + 'type': makeScalar('command'), + 'command': makeScalar('echo "start"'), + 'cwd': makeScalar('src'), + }), + ]), + }); + + const result = parseSubagentHooksFromYaml(hooksMap, workspaceRoot, userHome); + + assert.strictEqual(result[HookType.SessionStart]?.length, 1); + assert.deepStrictEqual(result[HookType.SessionStart]![0].cwd, URI.file('/workspace/src')); + }); + + test('skips non-sequence hook values', () => { + const hooksMap = makeMap({ + 'PreToolUse': makeScalar('not-a-sequence'), + }); + + const result = parseSubagentHooksFromYaml(hooksMap, workspaceRoot, userHome); + + assert.strictEqual(result[HookType.PreToolUse], undefined); + }); + }); }); 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 7d51bb6782f55..a5e0efdd72b44 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 @@ -794,6 +794,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local } }, @@ -850,6 +851,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local }, }, @@ -925,6 +927,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local } }, @@ -943,6 +946,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent2.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1013,6 +1017,7 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/github-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1031,6 +1036,7 @@ suite('PromptsService', () => { tools: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/vscode-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1049,6 +1055,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/generic-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1126,6 +1133,7 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/copilot-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1146,6 +1154,7 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.claude/agents/claude-agent.md'), source: { storage: PromptsStorage.local } }, @@ -1165,6 +1174,7 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.claude/agents/claude-agent2.md'), source: { storage: PromptsStorage.local } }, @@ -1221,6 +1231,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/demonstrate.md'), source: { storage: PromptsStorage.local } } @@ -1291,6 +1302,7 @@ suite('PromptsService', () => { argumentHint: undefined, target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/restricted-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1309,6 +1321,7 @@ suite('PromptsService', () => { tools: undefined, target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/no-access-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1327,6 +1340,7 @@ suite('PromptsService', () => { tools: undefined, target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, + hooks: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/full-access-agent.agent.md'), source: { storage: PromptsStorage.local } }, diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts index 7db5b9b60319e..f82b6bbe55dbd 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/askQuestionsTool.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { NullTelemetryService } from '../../../../../../../platform/telemetry/common/telemetryUtils.js'; import { NullLogService } from '../../../../../../../platform/log/common/log.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; -import { AskQuestionsTool, formatHeaderForDisplay, IAnswerResult, IQuestion, IQuestionAnswer } from '../../../../common/tools/builtinTools/askQuestionsTool.js'; +import { AskQuestionsTool, IAnswerResult, IQuestion, IQuestionAnswer } from '../../../../common/tools/builtinTools/askQuestionsTool.js'; import { IChatService } from '../../../../common/chatService/chatService.js'; class TestableAskQuestionsTool extends AskQuestionsTool { @@ -149,20 +149,4 @@ suite('AskQuestionsTool - convertCarouselAnswers', () => { assert.deepStrictEqual(result.answers['Case'], { selected: [], freeText: 'yes', skipped: false }); }); - - test('formats headers for carousel tab title display', () => { - assert.deepStrictEqual([ - formatHeaderForDisplay('FocusArea'), - formatHeaderForDisplay('UserValue'), - formatHeaderForDisplay('RiskLevel'), - formatHeaderForDisplay('Already Spaced'), - formatHeaderForDisplay('snake_case_header'), - ], [ - 'Focus area', - 'User value', - 'Risk level', - 'Already spaced', - 'Snake case header', - ]); - }); }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index 3caf43bdc4580..b70a49c4016b4 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -6,7 +6,6 @@ import { distinct } from '../../../../base/common/arrays.js'; import { CancelablePromise, createCancelablePromise, Promises, raceCancellablePromises, raceCancellation, timeout } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Codicon } from '../../../../base/common/codicons.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; @@ -14,13 +13,11 @@ import { isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IGalleryExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; import { IExtensionRecommendationNotificationService, IExtensionRecommendations, RecommendationsNotificationResult, RecommendationSource, RecommendationSourceToString } from '../../../../platform/extensionRecommendations/common/extensionRecommendations.js'; import { INotificationHandle, INotificationService, IPromptChoice, IPromptChoiceWithMenu, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { renderStronglyRecommendedExtensionList, StronglyRecommendedExtensionListResult } from './stronglyRecommendedExtensionList.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IUserDataSyncEnablementService, SyncResource } from '../../../../platform/userDataSync/common/userDataSync.js'; @@ -45,18 +42,6 @@ type ExtensionWorkspaceRecommendationsNotificationClassification = { const ignoreImportantExtensionRecommendationStorageKey = 'extensionsAssistant/importantRecommendationsIgnore'; const donotShowWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore'; -const stronglyRecommendedIgnoreStorageKey = 'extensionsAssistant/stronglyRecommendedIgnore'; -const stronglyRecommendedMajorVersionIgnoreStorageKey = 'extensionsAssistant/stronglyRecommendedMajorVersionIgnore'; - -interface MajorVersionIgnoreEntry { - readonly id: string; - readonly majorVersion: number; -} - -function parseMajorVersion(version: string): number { - const major = parseInt(version.split('.')[0], 10); - return isNaN(major) ? 0 : major; -} type RecommendationsNotificationActions = { onDidInstallRecommendedExtensions(extensions: IExtension[]): void; @@ -147,7 +132,6 @@ export class ExtensionRecommendationNotificationService extends Disposable imple constructor( @IConfigurationService private readonly configurationService: IConfigurationService, - @IDialogService private readonly dialogService: IDialogService, @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -484,159 +468,6 @@ export class ExtensionRecommendationNotificationService extends Disposable imple } } - async promptStronglyRecommendedExtensions(recommendations: Array): Promise { - if (this.hasToIgnoreRecommendationNotifications()) { - return; - } - - const ignoredList = this._getStronglyRecommendedIgnoreList(); - recommendations = recommendations.filter(rec => { - const key = isString(rec) ? rec.toLowerCase() : rec.toString(); - return !ignoredList.includes(key); - }); - if (!recommendations.length) { - return; - } - - let installed = await this.extensionManagementService.getInstalled(); - installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); - recommendations = recommendations.filter(recommendation => - installed.every(local => - isString(recommendation) ? !areSameExtensions({ id: recommendation }, local.identifier) : !this.uriIdentityService.extUri.isEqual(recommendation, local.location) - ) - ); - if (!recommendations.length) { - return; - } - - const allExtensions = await this.getInstallableExtensions(recommendations); - if (!allExtensions.length) { - return; - } - - const majorVersionIgnoreList = this._getStronglyRecommendedMajorVersionIgnoreList(); - const extensions = allExtensions.filter(ext => { - const ignored = majorVersionIgnoreList.find(e => e.id === ext.identifier.id.toLowerCase()); - return !ignored || parseMajorVersion(ext.version) > ignored.majorVersion; - }); - if (!extensions.length) { - return; - } - - const message = extensions.length === 1 - ? localize('stronglyRecommended1', "This workspace strongly recommends installing the '{0}' extension. Do you want to install?", extensions[0].displayName) - : localize('stronglyRecommendedN', "This workspace strongly recommends installing {0} extensions. Do you want to install?", extensions.length); - - let listResult!: StronglyRecommendedExtensionListResult; - - const { result } = await this.dialogService.prompt({ - message, - buttons: [ - { - label: localize('install', "Install"), - run: () => true, - }, - ], - cancelButton: localize('cancel', "Cancel"), - custom: { - icon: Codicon.extensions, - renderBody: (container, disposables) => { - listResult = renderStronglyRecommendedExtensionList(container, disposables, extensions); - }, - buttonOptions: [{ - styleButton: (button) => listResult.styleInstallButton(button), - }], - }, - }); - - if (result) { - const selected = extensions.filter(e => listResult.checkboxStates.get(e)); - const unselected = extensions.filter(e => !listResult.checkboxStates.get(e)); - if (unselected.length) { - this._addToStronglyRecommendedIgnoreList( - unselected.map(e => e.identifier.id) - ); - } - if (listResult.doNotShowAgainUnlessMajorVersionChange()) { - this._addToStronglyRecommendedIgnoreWithMajorVersion( - extensions.map(e => ({ id: e.identifier.id, majorVersion: parseMajorVersion(e.version) })) - ); - } - if (selected.length) { - const galleryExtensions: IGalleryExtension[] = []; - const resourceExtensions: IExtension[] = []; - for (const extension of selected) { - if (extension.gallery) { - galleryExtensions.push(extension.gallery); - } else if (extension.resourceExtension) { - resourceExtensions.push(extension); - } - } - await Promises.settled([ - Promises.settled(selected.map(extension => this.extensionsWorkbenchService.open(extension, { pinned: true }))), - galleryExtensions.length ? this.extensionManagementService.installGalleryExtensions(galleryExtensions.map(e => ({ extension: e, options: {} }))) : Promise.resolve(), - resourceExtensions.length ? Promise.allSettled(resourceExtensions.map(r => this.extensionsWorkbenchService.install(r))) : Promise.resolve(), - ]); - } - } - } - - private _getStronglyRecommendedIgnoreList(): string[] { - const raw = this.storageService.get(stronglyRecommendedIgnoreStorageKey, StorageScope.WORKSPACE); - if (raw === undefined) { - return []; - } - try { - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; - } - } - - private _addToStronglyRecommendedIgnoreList(recommendations: Array): void { - const list = this._getStronglyRecommendedIgnoreList(); - for (const rec of recommendations) { - const key = isString(rec) ? rec.toLowerCase() : rec.toString(); - if (!list.includes(key)) { - list.push(key); - } - } - this.storageService.store(stronglyRecommendedIgnoreStorageKey, JSON.stringify(list), StorageScope.WORKSPACE, StorageTarget.MACHINE); - } - - private _getStronglyRecommendedMajorVersionIgnoreList(): MajorVersionIgnoreEntry[] { - const raw = this.storageService.get(stronglyRecommendedMajorVersionIgnoreStorageKey, StorageScope.WORKSPACE); - if (raw === undefined) { - return []; - } - try { - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; - } - } - - private _addToStronglyRecommendedIgnoreWithMajorVersion(entries: MajorVersionIgnoreEntry[]): void { - const list = this._getStronglyRecommendedMajorVersionIgnoreList(); - for (const entry of entries) { - const key = entry.id.toLowerCase(); - const existing = list.findIndex(e => e.id === key); - if (existing !== -1) { - list[existing] = { id: key, majorVersion: entry.majorVersion }; - } else { - list.push({ id: key, majorVersion: entry.majorVersion }); - } - } - this.storageService.store(stronglyRecommendedMajorVersionIgnoreStorageKey, JSON.stringify(list), StorageScope.WORKSPACE, StorageTarget.MACHINE); - } - - resetStronglyRecommendedIgnoreState(): void { - this.storageService.remove(stronglyRecommendedIgnoreStorageKey, StorageScope.WORKSPACE); - this.storageService.remove(stronglyRecommendedMajorVersionIgnoreStorageKey, StorageScope.WORKSPACE); - } - private setIgnoreRecommendationsConfig(configVal: boolean) { this.configurationService.updateValue('extensions.ignoreRecommendations', configVal); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 9cba507bb0b56..12cf0a6c61fc1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -110,7 +110,6 @@ export class ExtensionRecommendationsService extends Disposable implements IExte this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations, this.extensionRecommendationsManagementService.onDidChangeIgnoredRecommendations)(() => this._onDidChangeRecommendations.fire())); this.promptWorkspaceRecommendations(); - this.promptStronglyRecommendedExtensions(); } private isEnabled(): boolean { @@ -275,15 +274,6 @@ export class ExtensionRecommendationsService extends Disposable implements IExte } } - private async promptStronglyRecommendedExtensions(): Promise { - const allowedRecommendations = this.workspaceRecommendations.stronglyRecommended - .filter(rec => !isString(rec) || this.isExtensionAllowedToBeRecommended(rec)); - - if (allowedRecommendations.length) { - await this.extensionRecommendationNotificationService.promptStronglyRecommendedExtensions(allowedRecommendations); - } - } - private _registerP(o: CancelablePromise): CancelablePromise { this._register(toDisposable(() => o.cancel())); return o; diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 218741bdd9969..37e6e916e167c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -1895,17 +1895,6 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi run: () => this.commandService.executeCommand('workbench.extensions.action.addToWorkspaceIgnoredRecommendations') }); - this.registerExtensionAction({ - id: 'workbench.extensions.action.resetStronglyRecommendedIgnoreState', - title: localize2('workbench.extensions.action.resetStronglyRecommendedIgnoreState', "Reset Strongly Recommended Extensions Ignore State"), - category: EXTENSIONS_CATEGORY, - menu: { - id: MenuId.CommandPalette, - when: WorkbenchStateContext.notEqualsTo('empty'), - }, - run: async (accessor: ServicesAccessor) => accessor.get(IExtensionRecommendationNotificationService).resetStronglyRecommendedIgnoreState() - }); - this.registerExtensionAction({ id: ConfigureWorkspaceRecommendedExtensionsAction.ID, title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' }, diff --git a/src/vs/workbench/contrib/extensions/browser/stronglyRecommendedExtensionList.ts b/src/vs/workbench/contrib/extensions/browser/stronglyRecommendedExtensionList.ts deleted file mode 100644 index c4cb34ce44332..0000000000000 --- a/src/vs/workbench/contrib/extensions/browser/stronglyRecommendedExtensionList.ts +++ /dev/null @@ -1,99 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { $, addDisposableListener } from '../../../../base/browser/dom.js'; -import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; -import { Emitter } from '../../../../base/common/event.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { localize } from '../../../../nls.js'; -import { ICustomDialogButtonControl } from '../../../../platform/dialogs/common/dialogs.js'; -import { defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; - -export interface StronglyRecommendedExtensionEntry { - readonly displayName: string; - readonly publisherDisplayName: string; - readonly version: string; -} - -export interface StronglyRecommendedExtensionListResult { - readonly checkboxStates: ReadonlyMap; - readonly hasSelection: boolean; - readonly doNotShowAgainUnlessMajorVersionChange: () => boolean; - styleInstallButton(button: ICustomDialogButtonControl): void; -} - -export function renderStronglyRecommendedExtensionList( - container: HTMLElement, - disposables: DisposableStore, - extensions: readonly T[], -): StronglyRecommendedExtensionListResult { - const checkboxStates = new Map(); - const onSelectionChanged = disposables.add(new Emitter()); - - container.style.display = 'flex'; - container.style.flexDirection = 'column'; - container.style.gap = '8px'; - container.style.padding = '8px 0'; - - const updateCheckbox = (ext: T, cb: Checkbox) => { - checkboxStates.set(ext, cb.checked); - onSelectionChanged.fire(); - }; - - for (const ext of extensions) { - checkboxStates.set(ext, true); - - const row = container.appendChild($('.strongly-recommended-extension-row')); - row.style.display = 'flex'; - row.style.alignItems = 'center'; - row.style.gap = '8px'; - - const cb = disposables.add(new Checkbox(ext.displayName, true, defaultCheckboxStyles)); - disposables.add(cb.onChange(() => updateCheckbox(ext, cb))); - row.appendChild(cb.domNode); - - const label = row.appendChild($('span')); - label.textContent = `${ext.displayName} v${ext.version} \u2014 ${ext.publisherDisplayName}`; - label.style.cursor = 'pointer'; - disposables.add(addDisposableListener(label, 'click', () => { - cb.checked = !cb.checked; - updateCheckbox(ext, cb); - })); - } - - const separator = container.appendChild($('div')); - separator.style.borderTop = '1px solid var(--vscode-widget-border)'; - separator.style.marginTop = '4px'; - separator.style.paddingTop = '4px'; - - const doNotShowRow = container.appendChild($('.strongly-recommended-do-not-show-row')); - doNotShowRow.style.display = 'flex'; - doNotShowRow.style.alignItems = 'center'; - doNotShowRow.style.gap = '8px'; - - const doNotShowCb = disposables.add(new Checkbox( - localize('doNotShowAgainUnlessMajorVersionChange', "Do not show again unless major version change"), - false, - defaultCheckboxStyles, - )); - doNotShowRow.appendChild(doNotShowCb.domNode); - - const doNotShowLabel = doNotShowRow.appendChild($('span')); - doNotShowLabel.textContent = localize('doNotShowAgainUnlessMajorVersionChange', "Do not show again unless major version change"); - doNotShowLabel.style.cursor = 'pointer'; - disposables.add(addDisposableListener(doNotShowLabel, 'click', () => { doNotShowCb.checked = !doNotShowCb.checked; })); - - const hasSelection = () => [...checkboxStates.values()].some(v => v); - - return { - checkboxStates, - get hasSelection() { return hasSelection(); }, - doNotShowAgainUnlessMajorVersionChange: () => doNotShowCb.checked, - styleInstallButton(button: ICustomDialogButtonControl) { - const updateEnabled = () => { button.enabled = hasSelection(); }; - disposables.add(onSelectionChanged.event(updateEnabled)); - }, - }; -} diff --git a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts index 8563dcc4c1537..69ff685c65837 100644 --- a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts @@ -25,9 +25,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { private _recommendations: ExtensionRecommendation[] = []; get recommendations(): ReadonlyArray { return this._recommendations; } - private _stronglyRecommended: Array = []; - get stronglyRecommended(): ReadonlyArray { return this._stronglyRecommended; } - private _onDidChangeRecommendations = this._register(new Emitter()); readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event; @@ -35,7 +32,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { get ignoredRecommendations(): ReadonlyArray { return this._ignoredRecommendations; } private workspaceExtensions: URI[] = []; - private workspaceExtensionIds = new Map(); private readonly onDidChangeWorkspaceExtensionsScheduler: RunOnceScheduler; constructor( @@ -94,12 +90,8 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { // ignore } } - this.workspaceExtensionIds.clear(); if (workspaceExtensions.length) { const resourceExtensions = await this.workbenchExtensionManagementService.getExtensions(workspaceExtensions); - for (const ext of resourceExtensions) { - this.workspaceExtensionIds.set(ext.identifier.id.toLowerCase(), ext.location); - } return resourceExtensions.map(extension => extension.location); } return []; @@ -118,7 +110,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { } this._recommendations = []; - this._stronglyRecommended = []; this._ignoredRecommendations = []; for (const extensionsConfig of extensionsConfigs) { @@ -142,24 +133,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { } } } - if (extensionsConfig.stronglyRecommended) { - for (const extensionId of extensionsConfig.stronglyRecommended) { - if (invalidRecommendations.indexOf(extensionId) === -1) { - const workspaceExtUri = this.workspaceExtensionIds.get(extensionId.toLowerCase()); - const extension = workspaceExtUri ?? extensionId; - const reason = { - reasonId: ExtensionRecommendationReason.Workspace, - reasonText: localize('stronglyRecommendedExtension', "This extension is strongly recommended by users of the current workspace.") - }; - this._stronglyRecommended.push(extension); - if (workspaceExtUri) { - this._recommendations.push({ extension: workspaceExtUri, reason }); - } else { - this._recommendations.push({ extension: extensionId, reason }); - } - } - } - } } for (const extension of this.workspaceExtensions) { @@ -179,7 +152,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { const invalidExtensions: string[] = []; let message = ''; - const allRecommendations = distinct(contents.flatMap(({ recommendations, stronglyRecommended }) => [...(recommendations || []), ...(stronglyRecommended || [])])); + const allRecommendations = distinct(contents.flatMap(({ recommendations }) => recommendations || [])); const regEx = new RegExp(EXTENSION_IDENTIFIER_PATTERN); for (const extensionId of allRecommendations) { if (regEx.test(extensionId)) { diff --git a/src/vs/workbench/contrib/extensions/common/extensionsFileTemplate.ts b/src/vs/workbench/contrib/extensions/common/extensionsFileTemplate.ts index 574806a1bc8f5..818e662847eea 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionsFileTemplate.ts +++ b/src/vs/workbench/contrib/extensions/common/extensionsFileTemplate.ts @@ -25,15 +25,6 @@ export const ExtensionsConfigurationSchema: IJSONSchema = { errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.") }, }, - stronglyRecommended: { - type: 'array', - description: localize('app.extensions.json.stronglyRecommended', "List of extensions that are strongly recommended for users of this workspace. Users will be prompted with a dialog to install these extensions. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."), - items: { - type: 'string', - pattern: EXTENSION_IDENTIFIER_PATTERN, - errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.") - }, - }, unwantedRecommendations: { type: 'array', description: localize('app.extensions.json.unwantedRecommendations', "List of extensions recommended by VS Code that should not be recommended for users of this workspace. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."), diff --git a/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts index 1dc178647978d..c3d16718ad3b2 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/installedMcpServersDiscovery.ts @@ -96,6 +96,7 @@ export class InstalledMcpServersDiscovery extends Disposable implements IMcpDisc env: config.env || {}, envFile: config.envFile, cwd: config.cwd, + sandbox: server.rootSandbox }; definitions[1].push({ @@ -103,7 +104,6 @@ export class InstalledMcpServersDiscovery extends Disposable implements IMcpDisc label: server.name, launch, sandboxEnabled: config.type === 'http' ? undefined : config.sandboxEnabled, - sandbox: config.type === 'http' ? undefined : config.sandbox, cacheNonce: await McpServerLaunch.hash(launch), roots: mcpConfigPath?.workspaceFolder ? [mcpConfigPath.workspaceFolder.uri] : undefined, variableReplacement: { diff --git a/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts index 3287e0347e7ca..fe7aca88798d1 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/nativeMcpDiscoveryAdapters.ts @@ -49,6 +49,7 @@ export async function claudeConfigToServerDefinition(idPrefix: string, contents: env: server.env || {}, envFile: undefined, cwd: cwd?.fsPath, + sandbox: undefined }; return { diff --git a/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts b/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts index a173a33b7d240..371bd1072154e 100644 --- a/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts +++ b/src/vs/workbench/contrib/mcp/common/discovery/pluginMcpDiscovery.ts @@ -100,6 +100,7 @@ export class PluginMcpDiscovery extends Disposable implements IMcpDiscovery { env: config.env ? { ...config.env } : {}, envFile: config.envFile, cwd: config.cwd, + sandbox: undefined, }; } diff --git a/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts b/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts index 0f8db744c9359..24fac57f1dea9 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpSandboxService.ts @@ -19,9 +19,8 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IMcpResourceScannerService, McpResourceTarget } from '../../../../platform/mcp/common/mcpResourceScannerService.js'; import { IRemoteAgentEnvironment } from '../../../../platform/remote/common/remoteAgentEnvironment.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; -import { IMcpSandboxConfiguration, IMcpStdioServerConfiguration, McpServerType } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; +import { IMcpSandboxConfiguration } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; import { IMcpPotentialSandboxBlock, McpServerDefinition, McpServerLaunch, McpServerTransportType } from './mcpTypes.js'; -import { Mutable } from '../../../../base/common/types.js'; export const IMcpSandboxService = createDecorator('mcpSandboxService'); @@ -85,7 +84,7 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService } if (await this.isEnabled(serverDef, remoteAuthority)) { this._logService.trace(`McpSandboxService: Launching with config target ${configTarget}`); - const launchDetails = await this._resolveSandboxLaunchDetails(configTarget, remoteAuthority, serverDef.sandbox, launch.cwd); + const launchDetails = await this._resolveSandboxLaunchDetails(configTarget, remoteAuthority, launch.sandbox, launch.cwd); const sandboxArgs = this._getSandboxCommandArgs(launch.command, launch.args, launchDetails.sandboxConfigPath); const sandboxEnv = this._getSandboxEnvVariables(launchDetails.tempDir, remoteAuthority); if (launchDetails.srtPath) { @@ -160,7 +159,7 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService let didChange = false; await this._mcpResourceScannerService.updateSandboxConfig(data => { - const existingSandbox = data.sandbox ?? serverDef.sandbox; + const existingSandbox = data.sandbox; const suggestedAllowedDomains = suggestedSandboxConfig?.network?.allowedDomains ?? []; const suggestedAllowWrite = suggestedSandboxConfig?.filesystem?.allowWrite ?? []; @@ -178,41 +177,24 @@ export class McpSandboxService extends Disposable implements IMcpSandboxService } } - didChange = currentAllowedDomains.size !== (existingSandbox?.network?.allowedDomains?.length ?? 0) - || currentAllowWrite.size !== (existingSandbox?.filesystem?.allowWrite?.length ?? 0); - - if (!didChange) { + if (suggestedAllowedDomains.length === 0 && suggestedAllowWrite.length === 0) { return data; } - const nextSandboxConfig: IMcpSandboxConfiguration = { - ...existingSandbox, - }; - - if (currentAllowedDomains.size > 0 || existingSandbox?.network?.deniedDomains?.length) { + didChange = true; + const nextSandboxConfig: IMcpSandboxConfiguration = {}; + if (currentAllowedDomains.size > 0) { nextSandboxConfig.network = { ...existingSandbox?.network, - allowedDomains: [...currentAllowedDomains], + allowedDomains: [...currentAllowedDomains] }; } - - if (currentAllowWrite.size > 0 || existingSandbox?.filesystem?.denyRead?.length || existingSandbox?.filesystem?.denyWrite?.length) { + if (currentAllowWrite.size > 0) { nextSandboxConfig.filesystem = { ...existingSandbox?.filesystem, allowWrite: [...currentAllowWrite], }; } - - //always remove sandbox at server level when writing back, it should only exist at the top level. This is to sanitize any old or malformed configs that may have sandbox defined at the server level. - if (data.servers) { - for (const serverName in data.servers) { - const serverConfig = data.servers[serverName]; - if (serverConfig.type === McpServerType.LOCAL) { - delete (serverConfig as Mutable).sandbox; - } - } - } - return { ...data, sandbox: nextSandboxConfig, diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 8ce685dfb1d48..4c77b0a2f549b 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -138,8 +138,6 @@ export interface McpServerDefinition { readonly staticMetadata?: McpServerStaticMetadata; /** Indicates if the sandbox is enabled for this server. */ readonly sandboxEnabled?: boolean; - /** Sandbox configuration to apply for this server. */ - readonly sandbox?: IMcpSandboxConfiguration; readonly presentation?: { @@ -173,7 +171,6 @@ export namespace McpServerDefinition { readonly variableReplacement?: McpServerDefinitionVariableReplacement.Serialized; readonly staticMetadata?: McpServerStaticMetadata; readonly sandboxEnabled?: boolean; - readonly sandbox?: IMcpSandboxConfiguration; } export function toSerialized(def: McpServerDefinition): McpServerDefinition.Serialized { @@ -188,7 +185,6 @@ export namespace McpServerDefinition { staticMetadata: def.staticMetadata, launch: McpServerLaunch.fromSerialized(def.launch), sandboxEnabled: def.sandboxEnabled, - sandbox: def.sandboxEnabled ? def.sandbox : undefined, variableReplacement: def.variableReplacement ? McpServerDefinitionVariableReplacement.fromSerialized(def.variableReplacement) : undefined, }; } @@ -202,8 +198,8 @@ export namespace McpServerDefinition { && objectsEqual(a.presentation, b.presentation) && objectsEqual(a.variableReplacement, b.variableReplacement) && objectsEqual(a.devMode, b.devMode) - && a.sandboxEnabled === b.sandboxEnabled - && objectsEqual(a.sandbox, b.sandbox); + && a.sandboxEnabled === b.sandboxEnabled; + } } @@ -519,6 +515,7 @@ export interface McpServerTransportStdio { readonly args: readonly string[]; readonly env: Record; readonly envFile: string | undefined; + readonly sandbox: IMcpSandboxConfiguration | undefined; } export interface McpServerTransportHTTPAuthentication { @@ -551,7 +548,7 @@ export type McpServerLaunch = export namespace McpServerLaunch { export type Serialized = | { type: McpServerTransportType.HTTP; uri: UriComponents; headers: [string, string][]; authentication?: McpServerTransportHTTPAuthentication } - | { type: McpServerTransportType.Stdio; cwd: string | undefined; command: string; args: readonly string[]; env: Record; envFile: string | undefined }; + | { type: McpServerTransportType.Stdio; cwd: string | undefined; command: string; args: readonly string[]; env: Record; envFile: string | undefined; sandbox: IMcpSandboxConfiguration | undefined }; export function toSerialized(launch: McpServerLaunch): McpServerLaunch.Serialized { return launch; @@ -569,6 +566,7 @@ export namespace McpServerLaunch { args: launch.args, env: launch.env, envFile: launch.envFile, + sandbox: launch.sandbox }; } } diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpIcons.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpIcons.test.ts index 7cdb2b661d0d2..90430507be1b5 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpIcons.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpIcons.test.ts @@ -22,7 +22,8 @@ const createStdioLaunch = (): McpServerTransportStdio => ({ command: 'cmd', args: [], env: {}, - envFile: undefined + envFile: undefined, + sandbox: undefined }); suite('MCP Icons', () => { diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts index e052e97d131e6..722a4ee2fe77f 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts @@ -239,6 +239,7 @@ suite('Workbench - MCP - Registry', () => { env: {}, envFile: undefined, cwd: '/test', + sandbox: undefined } }; }); @@ -301,6 +302,7 @@ suite('Workbench - MCP - Registry', () => { }, envFile: undefined, cwd: '/test', + sandbox: undefined }, variableReplacement: { section: 'mcp', @@ -402,6 +404,7 @@ suite('Workbench - MCP - Registry', () => { env: {}, envFile: undefined, cwd: '/test', + sandbox: undefined }, }; @@ -726,6 +729,7 @@ suite('Workbench - MCP - Registry', () => { env: {}, envFile: undefined, cwd: '/test', + sandbox: undefined } }; } diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts index 2648adedeb4c9..69b07fdf55f6f 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts @@ -171,7 +171,7 @@ export class TestMcpRegistry implements IMcpRegistry { serverDefinitions: observableValue(this, [{ id: 'test-server', label: 'Test Server', - launch: { type: McpServerTransportType.Stdio, command: 'echo', args: ['Hello MCP'], env: {}, envFile: undefined, cwd: undefined }, + launch: { type: McpServerTransportType.Stdio, command: 'echo', args: ['Hello MCP'], env: {}, envFile: undefined, cwd: undefined, sandbox: undefined }, cacheNonce: 'a', } satisfies McpServerDefinition]), trustBehavior: McpServerTrust.Kind.Trusted, diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts index 33114bc70d42b..44f180efe2f57 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts @@ -108,7 +108,8 @@ suite('Workbench - MCP - ServerConnection', () => { args: [], env: {}, envFile: undefined, - cwd: '/test' + cwd: '/test', + sandbox: undefined } }; }); diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts index b73a1d48fb5f0..d7a6d11283b50 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts @@ -40,7 +40,8 @@ suite('MCP Types', () => { command: 'test-command', args: [], env: {}, - envFile: undefined + envFile: undefined, + sandbox: undefined }, ...overrides }); @@ -89,7 +90,8 @@ suite('MCP Types', () => { command: 'command1', args: [], env: {}, - envFile: undefined + envFile: undefined, + sandbox: undefined } }); const def2 = createBasicDefinition({ @@ -99,7 +101,8 @@ suite('MCP Types', () => { command: 'command2', args: [], env: {}, - envFile: undefined + envFile: undefined, + sandbox: undefined } }); assert.strictEqual(McpServerDefinition.equals(def1, def2), false); diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index e0309cff01e5f..01010edd2afc5 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -385,9 +385,9 @@ .notebookOverlay .monaco-list-row .cell-title-toolbar { border-radius: var(--vscode-cornerRadius-medium); box-shadow: var(--vscode-shadow-sm); + background-color: var(--vscode-editorWidget-background); } -.notebookOverlay .monaco-list-row .cell-title-toolbar, .notebookOverlay .monaco-list-row.cell-drag-image, .notebookOverlay .cell-bottom-toolbar-container .action-item, .notebookOverlay .cell-list-top-cell-toolbar-container .action-item { diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index e8ab65c8a5ab7..b88b3073a9d01 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -62,6 +62,7 @@ .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget { margin-right: 3px; padding-bottom: 3px; + color: var(--vscode-descriptionForeground); } .settings-editor > .settings-header > .search-container > .search-container-widgets > .settings-count-widget:empty { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 19d8730711092..76b4bbb9c16e9 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -44,7 +44,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { defaultButtonStyles, defaultToggleStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { asCssVariable, asCssVariableWithDefault, badgeBackground, badgeForeground, contrastBorder, editorForeground, inputBackground } from '../../../../platform/theme/common/colorRegistry.js'; +import { asCssVariable, editorForeground } from '../../../../platform/theme/common/colorRegistry.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IUserDataSyncEnablementService, IUserDataSyncService, SyncStatus } from '../../../../platform/userDataSync/common/userDataSync.js'; import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; @@ -801,9 +801,6 @@ export class SettingsEditor2 extends EditorPane { this.controlsElement = DOM.append(this.searchContainer, DOM.$('.search-container-widgets')); this.countElement = DOM.append(this.controlsElement, DOM.$('.settings-count-widget.monaco-count-badge.long')); - this.countElement.style.backgroundColor = asCssVariable(badgeBackground); - this.countElement.style.color = asCssVariable(badgeForeground); - this.countElement.style.border = `1px solid ${asCssVariableWithDefault(contrastBorder, asCssVariable(inputBackground))}`; this.searchInputActionBar = this._register(new ActionBar(this.controlsElement, { actionViewItemProvider: (action, options) => { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 8b40d46937553..b158684ae0ea3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -13,6 +13,7 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IStorageService } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; @@ -60,6 +61,7 @@ export class TerminalEditor extends EditorPane { @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, @ITerminalService private readonly _terminalService: ITerminalService, @ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @IMenuService menuService: IMenuService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -156,7 +158,7 @@ export class TerminalEditor extends EditorPane { private _updateTabActionBar(profiles: ITerminalProfile[]): void { this._disposableStore.clear(); - const actions = getTerminalActionBarArgs(TerminalLocation.Editor, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore); + const actions = getTerminalActionBarArgs(TerminalLocation.Editor, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService); this._newDropdown.value?.update(actions.dropdownAction, actions.dropdownMenuActions); } @@ -180,7 +182,7 @@ export class TerminalEditor extends EditorPane { if (action instanceof MenuItemAction) { const location = { viewColumn: ACTIVE_GROUP }; this._disposableStore.clear(); - const actions = getTerminalActionBarArgs(location, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore); + const actions = getTerminalActionBarArgs(location, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService); this._newDropdown.value = this._instantiationService.createInstance(DropdownWithPrimaryActionViewItem, action, actions.dropdownAction, actions.dropdownMenuActions, actions.className, { hoverDelegate: options.hoverDelegate }); this._newDropdown.value?.update(actions.dropdownAction, actions.dropdownMenuActions); return this._newDropdown.value; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts index b0860ecc69cae..281d624fcec37 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts @@ -8,6 +8,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { Schemas } from '../../../../base/common/network.js'; import { localize, localize2 } from '../../../../nls.js'; import { IMenu, MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IExtensionTerminalProfile, ITerminalProfile, TerminalLocation, TerminalSettingId } from '../../../../platform/terminal/common/terminal.js'; import { ResourceContextKey } from '../../../common/contextkeys.js'; @@ -781,12 +782,20 @@ export function setupTerminalMenus(): void { } } -export function getTerminalActionBarArgs(location: ITerminalLocationOptions, profiles: ITerminalProfile[], defaultProfileName: string, contributedProfiles: readonly IExtensionTerminalProfile[], terminalService: ITerminalService, dropdownMenu: IMenu, disposableStore: DisposableStore): { +export function getTerminalActionBarArgs(location: ITerminalLocationOptions, profiles: ITerminalProfile[], defaultProfileName: string, contributedProfiles: readonly IExtensionTerminalProfile[], terminalService: ITerminalService, dropdownMenu: IMenu, disposableStore: DisposableStore, configurationService: IConfigurationService): { dropdownAction: IAction; dropdownMenuActions: IAction[]; className: string; dropdownIcon?: string; } { + const shouldElevateAiProfiles = configurationService.getValue(TerminalSettingId.ExperimentalAiProfileGrouping); + profiles = profiles.filter(e => !e.isAutoDetected); + const [aiProfiles, otherProfiles] = shouldElevateAiProfiles + ? splitProfiles(profiles) + : [[], profiles]; + const [aiContributedProfiles, otherContributedProfiles] = shouldElevateAiProfiles + ? splitContributedProfiles(contributedProfiles) + : [[], contributedProfiles]; const dropdownActions: IAction[] = []; const submenuActions: IAction[] = []; const splitLocation = (location === TerminalLocation.Editor || (typeof location === 'object' && hasKey(location, { viewColumn: true }) && location.viewColumn === ACTIVE_GROUP)) ? { viewColumn: SIDE_GROUP } : { splitActiveTerminal: true }; @@ -806,40 +815,22 @@ export function getTerminalActionBarArgs(location: ITerminalLocationOptions, pro location: splitLocation })))); dropdownActions.push(new Separator()); + for (const p of aiProfiles) { + addProfileActions(p, defaultProfileName, location, splitLocation, terminalService, dropdownActions, submenuActions, disposableStore); + } + for (const contributed of aiContributedProfiles) { + addContributedProfileActions(contributed, defaultProfileName, location, splitLocation, terminalService, dropdownActions, submenuActions, disposableStore); + } + if ((aiProfiles.length > 0 || aiContributedProfiles.length > 0) && (otherProfiles.length > 0 || otherContributedProfiles.length > 0)) { + dropdownActions.push(new Separator()); + } - profiles = profiles.filter(e => !e.isAutoDetected); - for (const p of profiles) { - const isDefault = p.profileName === defaultProfileName; - const options: ICreateTerminalOptions = { config: p, location }; - const splitOptions: ICreateTerminalOptions = { config: p, location: splitLocation }; - const sanitizedProfileName = p.profileName.replace(/[\n\r\t]/g, ''); - dropdownActions.push(disposableStore.add(new Action(TerminalCommandId.NewWithProfile, isDefault ? localize('defaultTerminalProfile', "{0} (Default)", sanitizedProfileName) : sanitizedProfileName, undefined, true, async () => { - await terminalService.createAndFocusTerminal(options); - }))); - submenuActions.push(disposableStore.add(new Action(TerminalCommandId.Split, isDefault ? localize('defaultTerminalProfile', "{0} (Default)", sanitizedProfileName) : sanitizedProfileName, undefined, true, async () => { - await terminalService.createAndFocusTerminal(splitOptions); - }))); + for (const p of otherProfiles) { + addProfileActions(p, defaultProfileName, location, splitLocation, terminalService, dropdownActions, submenuActions, disposableStore); } - for (const contributed of contributedProfiles) { - const isDefault = contributed.title === defaultProfileName; - const title = isDefault ? localize('defaultTerminalProfile', "{0} (Default)", contributed.title.replace(/[\n\r\t]/g, '')) : contributed.title.replace(/[\n\r\t]/g, ''); - dropdownActions.push(disposableStore.add(new Action('contributed', title, undefined, true, () => terminalService.createAndFocusTerminal({ - config: { - extensionIdentifier: contributed.extensionIdentifier, - id: contributed.id, - title - }, - location - })))); - submenuActions.push(disposableStore.add(new Action('contributed-split', title, undefined, true, () => terminalService.createAndFocusTerminal({ - config: { - extensionIdentifier: contributed.extensionIdentifier, - id: contributed.id, - title - }, - location: splitLocation - })))); + for (const contributed of otherContributedProfiles) { + addContributedProfileActions(contributed, defaultProfileName, location, splitLocation, terminalService, dropdownActions, submenuActions, disposableStore); } if (dropdownActions.length > 0) { @@ -852,3 +843,95 @@ export function getTerminalActionBarArgs(location: ITerminalLocationOptions, pro const dropdownAction = disposableStore.add(new Action('refresh profiles', localize('launchProfile', 'Launch Profile...'), 'codicon-chevron-down', true)); return { dropdownAction, dropdownMenuActions: dropdownActions, className: `terminal-tab-actions-${terminalService.resolveLocation(location)}` }; } + +function splitProfiles(profiles: readonly ITerminalProfile[]): [ITerminalProfile[], ITerminalProfile[]] { + const aiProfiles: ITerminalProfile[] = []; + const otherProfiles: ITerminalProfile[] = []; + for (const profile of profiles) { + if (isAiProfileName(profile.profileName)) { + aiProfiles.push(profile); + } else { + otherProfiles.push(profile); + } + } + return [aiProfiles, otherProfiles]; +} + +function splitContributedProfiles(contributedProfiles: readonly IExtensionTerminalProfile[]): [IExtensionTerminalProfile[], IExtensionTerminalProfile[]] { + const aiContributedProfiles: IExtensionTerminalProfile[] = []; + const otherContributedProfiles: IExtensionTerminalProfile[] = []; + for (const profile of contributedProfiles) { + if (isAiContributedProfile(profile)) { + aiContributedProfiles.push(profile); + } else { + otherContributedProfiles.push(profile); + } + } + return [aiContributedProfiles, otherContributedProfiles]; +} + +function isAiContributedProfile(profile: IExtensionTerminalProfile): boolean { + const extensionIdentifier = profile.extensionIdentifier.toLowerCase(); + if (extensionIdentifier === 'github.copilot-chat' || extensionIdentifier === 'anthropic.claude-code') { + return true; + } + + return isAiProfileName(profile.title); +} + +function isAiProfileName(name: string): boolean { + const lowerCaseName = name.toLowerCase(); + return lowerCaseName.includes('copilot') || lowerCaseName.includes('claude'); +} + +function addProfileActions( + profile: ITerminalProfile, + defaultProfileName: string, + location: ITerminalLocationOptions, + splitLocation: ITerminalLocationOptions, + terminalService: ITerminalService, + dropdownActions: IAction[], + submenuActions: IAction[], + disposableStore: DisposableStore +): void { + const isDefault = profile.profileName === defaultProfileName; + const options: ICreateTerminalOptions = { config: profile, location }; + const splitOptions: ICreateTerminalOptions = { config: profile, location: splitLocation }; + const sanitizedProfileName = profile.profileName.replace(/[\n\r\t]/g, ''); + dropdownActions.push(disposableStore.add(new Action(TerminalCommandId.NewWithProfile, isDefault ? localize('defaultTerminalProfile', "{0} (Default)", sanitizedProfileName) : sanitizedProfileName, undefined, true, async () => { + await terminalService.createAndFocusTerminal(options); + }))); + submenuActions.push(disposableStore.add(new Action(TerminalCommandId.Split, isDefault ? localize('defaultTerminalProfile', "{0} (Default)", sanitizedProfileName) : sanitizedProfileName, undefined, true, async () => { + await terminalService.createAndFocusTerminal(splitOptions); + }))); +} + +function addContributedProfileActions( + contributed: IExtensionTerminalProfile, + defaultProfileName: string, + location: ITerminalLocationOptions, + splitLocation: ITerminalLocationOptions, + terminalService: ITerminalService, + dropdownActions: IAction[], + submenuActions: IAction[], + disposableStore: DisposableStore +): void { + const isDefault = contributed.title === defaultProfileName; + const title = isDefault ? localize('defaultTerminalProfile', "{0} (Default)", contributed.title.replace(/[\n\r\t]/g, '')) : contributed.title.replace(/[\n\r\t]/g, ''); + dropdownActions.push(disposableStore.add(new Action('contributed', title, undefined, true, () => terminalService.createAndFocusTerminal({ + config: { + extensionIdentifier: contributed.extensionIdentifier, + id: contributed.id, + title + }, + location + })))); + submenuActions.push(disposableStore.add(new Action('contributed-split', title, undefined, true, () => terminalService.createAndFocusTerminal({ + config: { + extensionIdentifier: contributed.extensionIdentifier, + id: contributed.id, + title + }, + location: splitLocation + })))); +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 9a7457274151e..01632bee153f7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -289,7 +289,7 @@ export class TerminalViewPane extends ViewPane { case TerminalCommandId.New: { if (action instanceof MenuItemAction) { this._disposableStore.clear(); - const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore); + const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService); this._newDropdown.value = this._instantiationService.createInstance(DropdownWithPrimaryActionViewItem, action, actions.dropdownAction, actions.dropdownMenuActions, actions.className, { hoverDelegate: options.hoverDelegate, getKeyBinding: (action: IAction) => this._keybindingService.lookupKeybinding(action.id, this._contextKeyService) @@ -318,8 +318,15 @@ export class TerminalViewPane extends ViewPane { private _updateTabActionBar(profiles: ITerminalProfile[]): void { this._disposableStore.clear(); - const actions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore); + const actions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService); this._newDropdown.value?.update(actions.dropdownAction, actions.dropdownMenuActions); + + this._disposableStore.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(TerminalSettingId.ExperimentalAiProfileGrouping)) { + const updatedActions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService); + this._newDropdown.value?.update(updatedActions.dropdownAction, updatedActions.dropdownMenuActions); + } + })); } override focus() { diff --git a/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts index 6b6e849e3444d..6cdd98743ab35 100644 --- a/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts @@ -8,6 +8,7 @@ import { URI, UriComponents } from '../../../../../base/common/uri.js'; import { IChannel } from '../../../../../base/parts/ipc/common/ipc.js'; import { IWorkbenchConfigurationService } from '../../../../services/configuration/common/configuration.js'; import { IRemoteAuthorityResolverService } from '../../../../../platform/remote/common/remoteAuthorityResolver.js'; +import { IWorkbenchEnvironmentService } from '../../../../services/environment/common/environmentService.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { serializeEnvironmentDescriptionMap, serializeEnvironmentVariableCollection } from '../../../../../platform/terminal/common/environmentVariableShared.js'; import { IConfigurationResolverService } from '../../../../services/configurationResolver/common/configurationResolver.js'; @@ -111,6 +112,7 @@ export class RemoteTerminalChannelClient implements IPtyHostController { @ITerminalLogService private readonly _logService: ITerminalLogService, @IEditorService private readonly _editorService: IEditorService, @ILabelService private readonly _labelService: ILabelService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, ) { } restartPtyHost(): Promise { @@ -152,7 +154,13 @@ export class RemoteTerminalChannelClient implements IPtyHostController { } const resolverResult = await this._remoteAuthorityResolverService.resolveAuthority(this._remoteAuthority); - const resolverEnv = resolverResult.options && resolverResult.options.extensionHostEnv; + const resolverEnv = { + /** + * If the extension host was spawned via a launch configuration, + * include the environment provided by that launch configuration. + */ + ...(this._environmentService.debugExtensionHost.env ?? {}), ...resolverResult.options?.extensionHostEnv + }; const workspace = this._workspaceContextService.getWorkspace(); const workspaceFolders = workspace.folders; diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 632938e0d5dd9..132db6d298270 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -604,6 +604,15 @@ const terminalConfiguration: IStringDictionary = { mode: 'auto' } }, + [TerminalSettingId.ExperimentalAiProfileGrouping]: { + markdownDescription: localize('terminal.integrated.experimental.aiProfileGrouping', "Whether to elevate AI-contributed terminal profiles (for example Copilot CLI and Claude Agent) in the new terminal dropdown."), + type: 'boolean', + default: false, + tags: ['experimental'], + experiment: { + mode: 'auto' + } + }, [TerminalSettingId.ShellIntegrationEnabled]: { restricted: true, markdownDescription: localize('terminal.integrated.shellIntegration.enabled', "Determines whether or not shell integration is auto-injected to support features like enhanced command tracking and current working directory detection. \n\nShell integration works by injecting the shell with a startup script. The script gives VS Code insight into what is happening within the terminal.\n\nSupported shells:\n\n- Linux/macOS: bash, fish, pwsh, zsh\n - Windows: pwsh, git bash\n\nThis setting applies only when terminals are created, so you will need to restart your terminals for it to take effect.\n\n Note that the script injection may not work if you have custom arguments defined in the terminal profile, have enabled {1}, have a [complex bash `PROMPT_COMMAND`](https://code.visualstudio.com/docs/editor/integrated-terminal#_complex-bash-promptcommand), or other unsupported setup. To disable decorations, see {0}", '`#terminal.integrated.shellIntegration.decorationsEnabled#`', '`#editor.accessibilitySupport#`'), diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index d0bc952a87478..e8d460f796751 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -322,6 +322,13 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi case 'inspect-extensions': extensionHostDebugEnvironment.params.port = parseInt(value); break; + case 'extensionEnvironment': + try { + extensionHostDebugEnvironment.params.env = JSON.parse(value); + } catch (error) { + onUnexpectedError(error); + } + break; case 'enableProposedApi': extensionHostDebugEnvironment.extensionEnabledProposedApi = []; break; diff --git a/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts b/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts index ba02eaf679f9c..a48ce69a12bb2 100644 --- a/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts +++ b/src/vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig.ts @@ -24,7 +24,6 @@ export const EXTENSIONS_CONFIG = '.vscode/extensions.json'; export interface IExtensionsConfigContent { recommendations?: string[]; - stronglyRecommended?: string[]; unwantedRecommendations?: string[]; } @@ -36,7 +35,6 @@ export interface IWorkspaceExtensionsConfigService { readonly onDidChangeExtensionsConfigs: Event; getExtensionsConfigs(): Promise; getRecommendations(): Promise; - getStronglyRecommended(): Promise; getUnwantedRecommendations(): Promise; toggleRecommendation(extensionId: string): Promise; @@ -86,11 +84,6 @@ export class WorkspaceExtensionsConfigService extends Disposable implements IWor return distinct(configs.flatMap(c => c.recommendations ? c.recommendations.map(c => c.toLowerCase()) : [])); } - async getStronglyRecommended(): Promise { - const configs = await this.getExtensionsConfigs(); - return distinct(configs.flatMap(c => c.stronglyRecommended ? c.stronglyRecommended.map(c => c.toLowerCase()) : [])); - } - async getUnwantedRecommendations(): Promise { const configs = await this.getExtensionsConfigs(); return distinct(configs.flatMap(c => c.unwantedRecommendations ? c.unwantedRecommendations.map(c => c.toLowerCase()) : [])); @@ -303,7 +296,6 @@ export class WorkspaceExtensionsConfigService extends Disposable implements IWor private parseExtensionConfig(extensionsConfigContent: IExtensionsConfigContent): IExtensionsConfigContent { return { recommendations: distinct((extensionsConfigContent.recommendations || []).map(e => e.toLowerCase())), - stronglyRecommended: distinct((extensionsConfigContent.stronglyRecommended || []).map(e => e.toLowerCase())), unwantedRecommendations: distinct((extensionsConfigContent.unwantedRecommendations || []).map(e => e.toLowerCase())) }; } diff --git a/src/vs/workbench/test/browser/componentFixtures/stronglyRecommendedDialog.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/stronglyRecommendedDialog.fixture.ts deleted file mode 100644 index def925aad9c86..0000000000000 --- a/src/vs/workbench/test/browser/componentFixtures/stronglyRecommendedDialog.fixture.ts +++ /dev/null @@ -1,85 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Dialog } from '../../../../base/browser/ui/dialog/dialog.js'; -import { localize } from '../../../../nls.js'; -import { defaultButtonStyles, defaultCheckboxStyles, defaultDialogStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { StronglyRecommendedExtensionEntry, renderStronglyRecommendedExtensionList } from '../../../contrib/extensions/browser/stronglyRecommendedExtensionList.js'; -import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js'; - -export default defineThemedFixtureGroup({ - TwoExtensions: defineComponentFixture({ render: ctx => renderDialog(ctx, twoExtensions) }), - SingleExtension: defineComponentFixture({ render: ctx => renderDialog(ctx, singleExtension) }), - ManyExtensions: defineComponentFixture({ render: ctx => renderDialog(ctx, manyExtensions) }), - NoneSelected: defineComponentFixture({ render: ctx => renderDialog(ctx, twoExtensions, { allUnchecked: true }) }), -}); - -const twoExtensions: StronglyRecommendedExtensionEntry[] = [ - { displayName: 'TypeScript Customized Language Service', publisherDisplayName: 'Microsoft', version: '3.1.0' }, - { displayName: 'VS Code Extras', publisherDisplayName: 'Microsoft', version: '1.0.5' }, -]; - -const singleExtension: StronglyRecommendedExtensionEntry[] = [ - { displayName: 'TypeScript Customized Language Service', publisherDisplayName: 'Microsoft', version: '3.1.0' }, -]; - -const manyExtensions: StronglyRecommendedExtensionEntry[] = [ - { displayName: 'TypeScript Customized Language Service', publisherDisplayName: 'Microsoft', version: '3.1.0' }, - { displayName: 'VS Code Extras', publisherDisplayName: 'Microsoft', version: '1.0.5' }, - { displayName: 'ESLint', publisherDisplayName: 'Dirk Baeumer', version: '2.4.4' }, - { displayName: 'Prettier', publisherDisplayName: 'Esben Petersen', version: '10.1.0' }, - { displayName: 'GitLens', publisherDisplayName: 'GitKraken', version: '15.6.2' }, -]; - -function renderDialog({ container, disposableStore }: ComponentFixtureContext, extensions: StronglyRecommendedExtensionEntry[], options?: { allUnchecked?: boolean }): void { - container.style.width = '700px'; - container.style.height = '500px'; - container.style.position = 'relative'; - container.style.overflow = 'hidden'; - - // The dialog uses position:fixed on its modal block, which escapes the shadow DOM container. - // Override to position:absolute so it stays within the fixture bounds. - const fixtureStyle = new CSSStyleSheet(); - fixtureStyle.replaceSync('.monaco-dialog-modal-block { position: absolute; }'); - const shadowRoot = container.getRootNode() as ShadowRoot; - shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, fixtureStyle]; - - const message = extensions.length === 1 - ? localize('strongExtensionFixture', "This workspace strongly recommends installing the '{0}' extension. Do you want to install?", extensions[0].displayName) - : localize('strongExtensionsFixture', "This workspace strongly recommends installing {0} extensions. Do you want to install?", extensions.length); - - let listResult!: ReturnType; - - const dialog = disposableStore.add(new Dialog( - container, - message, - [ - localize('install', "Install"), - localize('cancel', "Cancel"), - ], - { - type: 'info', - renderBody: (bodyContainer: HTMLElement) => { - listResult = renderStronglyRecommendedExtensionList(bodyContainer, disposableStore, extensions); - }, - buttonOptions: [{ - styleButton: (button) => listResult.styleInstallButton(button), - }], - cancelId: 1, - buttonStyles: defaultButtonStyles, - checkboxStyles: defaultCheckboxStyles, - inputBoxStyles: defaultInputBoxStyles, - dialogStyles: defaultDialogStyles, - } - )); - - dialog.show(); - - if (options?.allUnchecked) { - for (const cb of container.querySelectorAll('.strongly-recommended-extension-row .monaco-custom-toggle')) { - cb.click(); - } - } -} diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 16c8ff488a027..839499ef55f04 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 14 +// version: 15 declare module 'vscode' { @@ -116,6 +116,13 @@ declare module 'vscode' { */ readonly parentRequestId?: string; + /** + * The permission level for tool auto-approval in this request. + * - `'autoApprove'`: Auto-approve all tool calls and retry on errors. + * - `'autopilot'`: Everything autoApprove does plus continues until the task is done. + */ + readonly permissionLevel?: string; + /** * Whether any hooks are enabled for this request. */ diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Dark.png new file mode 100644 index 0000000000000..6e97bb89568a6 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb56239cc915c18dbdf70d98049cc8386350c6e394b988a2df86df95ef10b52c +size 7064 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Light.png new file mode 100644 index 0000000000000..b8635ead60e58 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow1Line/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:163f0620ca91d6a7636ec58362e7dbc53a338fd26d2c9577ddb893c880bf86aa +size 7053 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Dark.png new file mode 100644 index 0000000000000..654a095b4442e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5d405d46064d7ae8cf9917587c51db8b80528590d4c9718729460781aa25ff9 +size 8657 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Light.png new file mode 100644 index 0000000000000..1350551e92918 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow2Lines/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02e137eff8bd38674c35f3d4ab472ff380bfa0c31e0f261e64a66822665c98a3 +size 8717 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Dark.png new file mode 100644 index 0000000000000..9aa34fcadd401 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:029ca626c8b89d7efce7d86b401774373d473282fa29b45f2a0b6eff314090da +size 8737 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Light.png new file mode 100644 index 0000000000000..11afd7ebade85 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3Lines/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98519a1e1420101c2efb7fedf417432aa9509b3614d0df4fd51e57b02f791a9c +size 8684 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Dark.png new file mode 100644 index 0000000000000..23cae64ce7235 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3619142a6234cafb5b20bae5007daf1af201bb58f8b0f1e7e404621a77d74123 +size 12355 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Light.png new file mode 100644 index 0000000000000..fbe36047f39b7 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow3LongLines/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:717467794a22315bb378be412b331db00635d0171043deb50fa6e50a3caf9af3 +size 12363 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Dark.png new file mode 100644 index 0000000000000..3f0813226820f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a68b3828da8ddd4254a8ff14d740d07ed9463012a6646d466ad052f917080462 +size 9167 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Light.png new file mode 100644 index 0000000000000..e3e96fe51a859 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRow4Lines/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29a3581395521fe377cb76135f9df237ba8d6d241b5bcee4707a4c92e560e410 +size 9169 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Dark.png new file mode 100644 index 0000000000000..c6025168a8c78 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:349e1054718733508b1e8ccab0c08039e13319458a1a4e730d98531c9f8065a3 +size 7889 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Light.png new file mode 100644 index 0000000000000..c9bdeeb188dd9 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowBash/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49dd6690cb406f928925ad0000624128efc5e60999a6472f5c14f5d34a048b8a +size 7940 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Dark.png new file mode 100644 index 0000000000000..29f4c5e28a704 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfdc23fd51094966957b9487c71aa7dc69d9ace05bc9315adf6e8ae296de3d89 +size 7338 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Light.png new file mode 100644 index 0000000000000..7684550a5f6fb --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowJson/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0fc1e6917fd33d4c46dd70b74d787bc0c2bc2b2d6f38c1d1c8923ca84b11009 +size 7439 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Dark.png new file mode 100644 index 0000000000000..40cf1fbcec966 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6827c363512a28c506670d9bce96efe463bb2e7b16864c1d3ef78bdb27c5d8b9 +size 7915 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Light.png new file mode 100644 index 0000000000000..035820555ccb3 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowLongLabel/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d90a5b77afd766a79dc78ec15369ae6fd8d37791f3b365cd51293b86089a268 +size 8005 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Dark.png new file mode 100644 index 0000000000000..843641053a15a --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10b958efa32aa1dd506894c1663639bfd8252907b3640e4d2c6fb1893f0798dc +size 7497 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Light.png new file mode 100644 index 0000000000000..4f679b73ace38 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ApprovalRowPowerShell/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:083d37a41eb68a5f0841c4461d4ee9df3e5e093b7295d069877fce8933c30581 +size 7548 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Dark.png new file mode 100644 index 0000000000000..18b0f2f3393b1 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcb0046c2d6bf62981df0f57af2f04bc21abff8f2c25bda0bce72577c8825234 +size 3955 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Light.png new file mode 100644 index 0000000000000..0518b7db98733 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/Archived/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6955c3424c2907b6b8472c60e5c5cee6ef104dd8482fd932e0c83ba611c3522b +size 3883 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Dark.png new file mode 100644 index 0000000000000..2e9a78f3e9c48 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:761643f249dddaaa1e7f307b3372463e252f67ed30cdd860052ca2b1b9534601 +size 4136 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Light.png new file mode 100644 index 0000000000000..c4edc35a16909 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ArchivedUnread/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed79c20b8c4adb9c4253ebed499554e3d8e898b0b5f9d389c8ce865345b85ad6 +size 4059 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Dark.png new file mode 100644 index 0000000000000..2f4f1029af5d0 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:211c5915f952e93eede04b6ff57b552ccfd7a524ec17ded20819f531e07289de +size 4323 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Light.png new file mode 100644 index 0000000000000..b5a83bc3227a9 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/BackgroundProvider/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76981d631106433e066dd7b6354337c3bbba1018587beb26ebd9d662f25093c0 +size 4440 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Dark.png new file mode 100644 index 0000000000000..64427aeaa2387 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd6f5753251dfd4ebad48ac881d2a18baeeb6dac683d8c991b23676d80e79f7d +size 4627 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Light.png new file mode 100644 index 0000000000000..ff344e500fe65 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/ClaudeProvider/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46d1010d543349b526f2b6cdfc17177fdda11cc994a6522c6a827c883755e21b +size 4720 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Dark.png new file mode 100644 index 0000000000000..2ffb6d1dfa933 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a357eca1ff0edae842d2137e53c4648a8a24dda806056f7cbb047ea02bc05250 +size 4402 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Light.png new file mode 100644 index 0000000000000..c7f3c6cdfdbc8 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProvider/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4127bb1d38d3c49f0393afcf4d04415f328cccbe8473de7e0894b671f5c6ec51 +size 4475 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Dark.png new file mode 100644 index 0000000000000..94ecc18661323 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11e38388dbe35d160b60aa8d8b1b45b2a71c9604caed3b610b318f4fc5beb5c0 +size 3746 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Light.png new file mode 100644 index 0000000000000..6c780125a43c0 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CloudProviderInProgress/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:768a1bfc002b37facc1d7e7c4a096148134b2f037f6739611b0ebdac8694515e +size 3741 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Dark.png new file mode 100644 index 0000000000000..9379588d30350 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ef5c71cd24e7527c325436f778498f4ce843c4118fb6df9132d152d195b77b5 +size 4131 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Light.png new file mode 100644 index 0000000000000..cc372129852b4 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedRead/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:461481ef366395b7a3283de9dbac92581b182420f6439d15af3c5ad1b95f315f +size 4236 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Dark.png new file mode 100644 index 0000000000000..e7b1e95cc9b39 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29611b9811d052d0c311abf299aaf4009eb4cf9adc39b50a362f23dbb30e3012 +size 4281 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Light.png new file mode 100644 index 0000000000000..53e514e305ba8 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/CompletedUnread/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cafcc1fb16a92c55106474771945cc7063152922633db3d3acc8f455677e93a2 +size 4303 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Dark.png new file mode 100644 index 0000000000000..0f129c39bf11c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f18d15f95ce04361389e3685c0d04d5b5846ee7ac97f192ad75605879e3ef711 +size 3952 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Light.png new file mode 100644 index 0000000000000..b2c232971dbb0 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithDuration/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:823534baac31274e97febf864f4c70430672f573a35abe360c406f0ef8394563 +size 3984 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Dark.png new file mode 100644 index 0000000000000..60ae250bdb330 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99a4986f49d637f978fdef48c75c7307b1d2fe60c0a301682481a2d9271fe9c0 +size 3473 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Light.png new file mode 100644 index 0000000000000..77c3257a44b77 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/FailedWithoutDuration/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e0c6543a97ca59eeb9bced9530976e6697e31df4cc0e392dae5721cdef90a35 +size 3590 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Dark.png new file mode 100644 index 0000000000000..cf64cff29d7a8 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39e4ce2cd4020f790d3a2f54ab29da693ce34b35a82a4ef9ebbc7e685b6c5f29 +size 3942 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Light.png new file mode 100644 index 0000000000000..4e2ed61233f8d --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgress/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c0310d172ee83a1f0260a7731e321a8e193dbb4c58afa0115bf3e2246b623db +size 3997 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Dark.png new file mode 100644 index 0000000000000..75e19d712784e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9a68bfda118629ddd5095f2fdd202c5baaccbc6b92d1d9584ef5053113af328 +size 4693 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Light.png new file mode 100644 index 0000000000000..bc5cbfc0de728 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/InProgressWithDescription/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:616343f0b523bd7a88c02349345baedebbccb3251b34030903e5afe54c14ee0d +size 4793 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Dark.png new file mode 100644 index 0000000000000..50cbb3efca44d --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea3e3728408c21a161655ad68304442ea348f4c95b613e5abc5be491073d5a11 +size 3767 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Light.png new file mode 100644 index 0000000000000..82d68f0fd0d81 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/NeedsInput/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78eb1956b1a37699a678f73d79cb6c1d7b4deda79e857c03017910054fdbae90 +size 3815 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Dark.png new file mode 100644 index 0000000000000..60acf6fe15b4b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67e30acb57253a7a9c02e93e8120019eb1afc9932a99c59a241968ae75ee3752 +size 896 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Light.png new file mode 100644 index 0000000000000..44015ee388160 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionArchived/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d6a95e55db9415c49043745f6eb5682e20137d0d487edb6323ef101c2e46016 +size 870 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Dark.png new file mode 100644 index 0000000000000..130f5f1c40a42 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:379ebb29923b4874351469a1a60a7e4bc76397c85e61741dc86361dd123f68d0 +size 1032 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Light.png new file mode 100644 index 0000000000000..cc2acbb591696 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionLastWeek/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:109ad01629c5fb42759102c4a9fdf3b9e4f69452d06d6b2f6b478be418261e01 +size 1013 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Dark.png new file mode 100644 index 0000000000000..da3fbefb55caa --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d1c9f3fba50deacb99898e7e403239716b918e4901258b155f3c87558938219 +size 634 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Light.png new file mode 100644 index 0000000000000..1fd51cdd988ad --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionMore/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9083bcd9e6dd0fddf1b86389abbf2c8be5db4c84a80870acee1ebb6209b3734c +size 610 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Dark.png new file mode 100644 index 0000000000000..5a3f629e0d966 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fb24f8743a805c51eed4cb0822e6f031f1663ebf3860fa0f555fc9105d6aa47 +size 655 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Light.png new file mode 100644 index 0000000000000..9a01d0f11bdc9 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionOlder/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b33d4a2df45aadb2ddd03a4e78107993407390a0fcdcd551cc5e46cfe3bfd79 +size 629 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Dark.png new file mode 100644 index 0000000000000..7df9b9dc3e352 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8dba102faa5ab22be300c2f3b144ccb9fd7d191b7ce345934aac0cf2a22a4cf2 +size 700 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Light.png new file mode 100644 index 0000000000000..97bfabf75bb41 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionToday/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0896ebcac18fc2c67051318eccf952a4af9cded093bb8ed22da0d4f369a90fd +size 691 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Dark.png new file mode 100644 index 0000000000000..44f782bea7336 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf4cc56f6e7472b91ee5291f7b7963c88c575152c01914af66b79d3f983072a1 +size 1034 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Light.png new file mode 100644 index 0000000000000..fd7a76b1e5590 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/SectionYesterday/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f11009c1529cb97619d437c5563764d6cf9deed1db7d7b539506a8500d58694d +size 1011 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Dark.png new file mode 100644 index 0000000000000..e8a6dcc8dde98 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5dfad96ea810926d39db4d8f689f66c717fd4a2b51de380010046e7610ec14a4 +size 4819 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Light.png new file mode 100644 index 0000000000000..7605ff11511a5 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadge/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b6b40e3ab4d73221357a136dcac51eb0081c344a98930dd5e738662137af955 +size 4886 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Dark.png new file mode 100644 index 0000000000000..5e3bbe18e1e93 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:768613c86e446829341642343180cd84a0376dfafae7399dc2a50cf3b0e21575 +size 3900 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Light.png new file mode 100644 index 0000000000000..3b1b9440317ef --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithBadgeAndDiff/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d471d8eeb3bcf0a285a3b80a7dac70d5258c7fee903185377bddad1199562259 +size 4018 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Dark.png new file mode 100644 index 0000000000000..842e5652928af --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83b83d1b89bbee53b1e7f81990b30e8f2124b2099e0da948bcc0e807d2593f1e +size 5389 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Light.png new file mode 100644 index 0000000000000..a15244fa8b96e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDescription/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39cd38f065604d4c1ea6564d0da9cd5436e7587547583783408c7e7df3812f80 +size 5507 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Dark.png new file mode 100644 index 0000000000000..760959402ecf3 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0ea6dc5746ca3dba73ee8a96df90fd2c4f34625d58ac9d616134ba5d2ff5f87 +size 3473 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Light.png new file mode 100644 index 0000000000000..14d28a1f9995d --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithDiffChanges/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d502f79bc59e00f539745df156fa704cc3db11c16653633943b6edf26401e25 +size 3617 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Dark.png new file mode 100644 index 0000000000000..10de39113fef8 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e652cd030f299556fa1fc6455533c38ac953cae0bd472248aed8034d7f19a12e +size 3129 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Light.png new file mode 100644 index 0000000000000..980b2d57abadc --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithFileChangesList/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a189aa2bcce54111a8c078985ca611efde6e10dde26bb227bf1afafa2d065cc0 +size 3250 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Dark.png new file mode 100644 index 0000000000000..0a49729406425 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c44f892307b93c5bb48b836d47c5c66a318e299ed59ae96ca8107eecb5ad1012 +size 5621 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Light.png new file mode 100644 index 0000000000000..86f7c7e31451b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownBadge/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82245981f37c430eab84149196ccc9769fc90347b1b8fc28942dcdd7d3a5357c +size 5711 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Dark.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Dark.png new file mode 100644 index 0000000000000..456cc163ea32f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b702e865b2a24a3ebf2028662a7aa1f9dc4176e90761aa50627736a65f4a8000 +size 5368 diff --git a/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Light.png b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Light.png new file mode 100644 index 0000000000000..efa1d0dbda732 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/agentSessionsViewer/WithMarkdownDescription/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cf08481a25b64169c03ee6321326f2108ed37a08b4ade5e75c7b3ff13e63ce7 +size 5393 diff --git a/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHover/Dark.png b/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHover/Dark.png deleted file mode 100644 index 89032243eae7f..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHover/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cea3d365efe7e033cfd1fb8bc408fa0853d769af9cf7fcd8c1217d7c1e7982ba -size 15184 diff --git a/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHover/Light.png b/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHover/Light.png deleted file mode 100644 index dde67257b6050..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHover/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3503662354e0c76df5ed180db789547fa2a1b1099892d9628df8f5510a0d385b -size 14312 diff --git a/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Dark.png new file mode 100644 index 0000000000000..bfe8d842cf7fe --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0f41bf819fed7de2b99f15776cdb0d353f9bfaa4526dbb857f12be7c1343881 +size 15185 diff --git a/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Light.png b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Light.png new file mode 100644 index 0000000000000..914374bffd9ee --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHover/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50328c742ab8f5d52b66bc5e693277733be6adb62262925c16527e92b52bf47a +size 14309 diff --git a/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHoverNoData/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Dark.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHoverNoData/Dark.png rename to test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Dark.png diff --git a/test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHoverNoData/Light.png b/test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Light.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/aiStats/AiStatsHoverNoData/Light.png rename to test/componentFixtures/.screenshots/baseline/chat/aiStats/AiStatsHoverNoData/Light.png diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Dark.png new file mode 100644 index 0000000000000..7093082f758ec --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84ff989328d86ec68ab4b2771e8798051794c85c80e830e6061bf7b538dbe342 +size 1998 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Light.png new file mode 100644 index 0000000000000..e490a13bc02f6 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/Completed/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d590d4f26fb679fc1c0be2031660fc0568d0d5512e8f7272c81ad6932cde7079 +size 1994 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Dark.png new file mode 100644 index 0000000000000..5df4b562cf9fa --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfc063153b0a031ec15142d02b494acf778323b3f77c2d4a470936c8f52ac481 +size 7895 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Light.png new file mode 100644 index 0000000000000..942afa17055e8 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/LongMessage/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f61d72a6e32d7f82c198c827d999462dbb84f3154220ff9b96b2339b9c2ee4f7 +size 7832 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Dark.png new file mode 100644 index 0000000000000..993c9cdea973b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f4b1725f22fe74e252757ef5e3011112c86831087eb0762518295af64f085da +size 1604 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Light.png new file mode 100644 index 0000000000000..fa997f05c8792 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithCustomIcon/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecf877a05b5bab1f506ef7db0369e535434ef4ec712ae6cfc1e34aa62defd492 +size 1567 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Dark.png new file mode 100644 index 0000000000000..b09bad6fccd11 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0ab968d64c65fbd57da2751037b3795dd44edfdb4288e1b8164affce136d694 +size 4924 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Light.png new file mode 100644 index 0000000000000..b55193ca7a0d2 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithInlineCode/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:715427f1205744485b04ca1e1c2371116d53cc0078ba7b59b39ce492c64499ae +size 4901 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Dark.png new file mode 100644 index 0000000000000..4ce32cefd9161 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5674c1f5e9cfbf497eec8dccb23abb9bace0742e74804ea752b2fd652bdb75a3 +size 3541 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Light.png new file mode 100644 index 0000000000000..65dca4cf3688b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatProgressContentPart/WithSpinner/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:857f0f66eac110ae84befc845a8557fd85d64d3c9b3c38fb3f1390b98e5eea47 +size 3567 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Dark.png new file mode 100644 index 0000000000000..eed451005b59f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bf0c0deb7567010db027a90da11dbe87b2e0e0e1a19b80bde4ae389363451d3 +size 15762 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Light.png new file mode 100644 index 0000000000000..5822d248ad966 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultiSelectQuestion/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1aa1238a25cb27637f4e6505dbbdcb2a4d732a45d6238a46f55205bb9db1227f +size 16001 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Dark.png new file mode 100644 index 0000000000000..41af30d168d76 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d37f98e5c7c4534b0500f0ee559fccea0136b53bafd5ae69cd5c5005fd1bbad +size 9914 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Light.png new file mode 100644 index 0000000000000..b59acaf8d2592 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/MultipleQuestions/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70cb69e09936cfa033136995be7ab2d25ddc73fa4043004953d4c7b685adff1c +size 9803 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Dark.png new file mode 100644 index 0000000000000..111aefc055700 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1e88af6f572f98d7110e3efd9c7980446aeee5dc46bc30ae033130788da222e +size 24999 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Light.png new file mode 100644 index 0000000000000..f74bfcb0b0da3 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/NoSkip/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89e45041078c82051075e4882b4a8beffae1298d3610270c9d95049c5f05c5b9 +size 25094 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Dark.png new file mode 100644 index 0000000000000..55185e54f2ab2 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15e7ffe28aa3ddcaf263df3d829825c6941274a67d454c3416ebdfdc56c7b2fb +size 25463 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Light.png new file mode 100644 index 0000000000000..1457a5c435990 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleSelectQuestion/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f33c81825ee8ebb528dfdc7dacd1689dd28dedf2250faaf38ca21db82f9bd25 +size 25513 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Dark.png new file mode 100644 index 0000000000000..a4e786b07a040 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29edbbb4350570a92063105121d358bb82557d02e297783ac9aa94ca5857db99 +size 10082 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Light.png new file mode 100644 index 0000000000000..fc33b965edfd5 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SingleTextQuestion/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aafbf7aab60ca0474cd01a36192fa4faeab7eeb3b9e7d432121f1ae977c1441e +size 10033 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Dark.png new file mode 100644 index 0000000000000..bd57128f48894 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:183fd97931b5f9edd0cdf7c6374328f6fa12ac728246c4156b8416f68f9d6960 +size 1954 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Light.png new file mode 100644 index 0000000000000..3c0dbd6ae6b7e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SkippedSummary/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa5471c7656b76e40d6e7123ee8506aeb9a4b42a7157b9bf24a02e117017b6cc +size 1918 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Dark.png new file mode 100644 index 0000000000000..b5cab20b4ff42 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e725fcfce7606554463ee6aa335a3530a6e37935b2a163e78d940b3557e58d6 +size 17933 diff --git a/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Light.png b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Light.png new file mode 100644 index 0000000000000..12a8743c86ec4 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/chatQuestionCarousel/SubmittedSummary/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90b08fe37901ea04d0b815f73014168cb2ec0643c4e7f8fa8993783384e884b3 +size 16311 diff --git a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png new file mode 100644 index 0000000000000..b12335959e1d3 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4bf1714a905c76cc16aae7c5b7f20c6418018e0820e946831348797c13bb2a4 +size 27743 diff --git a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png new file mode 100644 index 0000000000000..2d24452c3049c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:153095490477eaa37e4a0648c41e30e7ba8d62dd699a6e3cf3ec04399bbfa91a +size 27090 diff --git a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Dark.png b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Dark.png new file mode 100644 index 0000000000000..aeab5d111241f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7b2e11c4f828e0fbf8ca8ef0c7607965aa1b974280851806a038a45f04b9de9 +size 23556 diff --git a/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Light.png b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Light.png new file mode 100644 index 0000000000000..6f13702b87e5e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/chat/promptFilePickers/PromptFiles/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb5027255ce762a6de6990d20d43a0aae0365ce3e2ca69e374b9b8791a4d758e +size 23201 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultiSelectQuestion/Dark.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultiSelectQuestion/Dark.png deleted file mode 100644 index b5f1d62d15e21..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultiSelectQuestion/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:05f0f518e03f8b91e2178761c977215bd2561b10b04ad69685aa054231cd81be -size 15058 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultiSelectQuestion/Light.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultiSelectQuestion/Light.png deleted file mode 100644 index 8d17c778055ca..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultiSelectQuestion/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2c5de42c54bcd1d35f59711f2c8c9c24f747df926017f1c3a52e3703c9d69da7 -size 15326 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultipleQuestions/Dark.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultipleQuestions/Dark.png deleted file mode 100644 index 597e7cbbc672e..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultipleQuestions/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7510376219beff4c9bb17bccb3de3631a633bda27fe53d6418309de484167688 -size 7506 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultipleQuestions/Light.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultipleQuestions/Light.png deleted file mode 100644 index 93470614a41e7..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/MultipleQuestions/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8918a56e08760211087b9cd0c798758cb7a55c002fcce1b343d06fd9f078197f -size 7434 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/NoSkip/Dark.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/NoSkip/Dark.png deleted file mode 100644 index 356200049e46c..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/NoSkip/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a324c6cde2596228ddc260d04688bf5153860b7e7de5b41f9211b456285ee581 -size 25804 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/NoSkip/Light.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/NoSkip/Light.png deleted file mode 100644 index 2b1bfefce81b7..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/NoSkip/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0eb74e618241b7d863bf8bac7132204b332c47e44e15a9c0b12270fa31a4fb71 -size 25874 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleSelectQuestion/Dark.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleSelectQuestion/Dark.png deleted file mode 100644 index 1ecda11f8e889..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleSelectQuestion/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1d5ede2eec1393f07f015cd724e9bb0a84492d85b175577705e195ee948e80ab -size 26210 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleSelectQuestion/Light.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleSelectQuestion/Light.png deleted file mode 100644 index 5025b7c37cdcf..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleSelectQuestion/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:695e11e18a05b7f9d0b73612edc7f4b0288408bedbf7fc259fccb2c7fe5f7dd0 -size 26288 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleTextQuestion/Dark.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleTextQuestion/Dark.png deleted file mode 100644 index 2bc00d0ba8436..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleTextQuestion/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d03152872902d21cea55c0d8ba464894c9a6ec77e7a58f68360c3be26a9a62a4 -size 7302 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleTextQuestion/Light.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleTextQuestion/Light.png deleted file mode 100644 index 26a90f10d7af8..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SingleTextQuestion/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d727ac8876b301c7bf81d799197b6a8b0962a95bc9eb9b269e28f6c1eb4acd88 -size 7246 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SkippedSummary/Dark.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SkippedSummary/Dark.png deleted file mode 100644 index ccf46b5de69d9..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SkippedSummary/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a3e31fb939e811c76b4fb03a7211cc84b86794dbdce2ecf26d1d42324dc86fd4 -size 2099 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SkippedSummary/Light.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SkippedSummary/Light.png deleted file mode 100644 index 29bea3c013db4..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SkippedSummary/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8d7fd7e2f20d9a7a8e2ea867e0383e88291547b5e9d2c04642a84b5087491f64 -size 2064 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SubmittedSummary/Dark.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SubmittedSummary/Dark.png deleted file mode 100644 index 7b0b53c68a447..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SubmittedSummary/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c84efc6a1c010c303da05377ae980ead28d0f7412d0ce0e49bfc66ce23c766d3 -size 20333 diff --git a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SubmittedSummary/Light.png b/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SubmittedSummary/Light.png deleted file mode 100644 index 858f4e5ccd34d..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/chatQuestionCarousel/SubmittedSummary/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1cf2233bec9a1ab64995e6168081da43e485d6b2388f868648613d63034160b8 -size 18278 diff --git a/test/componentFixtures/.screenshots/baseline/codeActionList/GroupedCodeActions/Dark.png b/test/componentFixtures/.screenshots/baseline/codeActionList/GroupedCodeActions/Dark.png deleted file mode 100644 index b48c12e655ed1..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/codeActionList/GroupedCodeActions/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b99678e6d41e30c874e0517eb6d97ccb3eaece31c489482fae81dd92904051c9 -size 14963 diff --git a/test/componentFixtures/.screenshots/baseline/codeActionList/GroupedCodeActions/Light.png b/test/componentFixtures/.screenshots/baseline/codeActionList/GroupedCodeActions/Light.png deleted file mode 100644 index 8b2121d3a7c79..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/codeActionList/GroupedCodeActions/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bfaa85a662524e06f3d8323bf9a4655822d36567ffa438db85719a79addb6cd3 -size 14439 diff --git a/test/componentFixtures/.screenshots/baseline/codeActionList/SimpleQuickFixes/Dark.png b/test/componentFixtures/.screenshots/baseline/codeActionList/SimpleQuickFixes/Dark.png deleted file mode 100644 index f82d6f544e6cc..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/codeActionList/SimpleQuickFixes/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:38e0cc34e642669cd200ac977f5e8948c100c100457f202b20a642ca211ab959 -size 6540 diff --git a/test/componentFixtures/.screenshots/baseline/codeActionList/SimpleQuickFixes/Light.png b/test/componentFixtures/.screenshots/baseline/codeActionList/SimpleQuickFixes/Light.png deleted file mode 100644 index ecbb1c1dc430c..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/codeActionList/SimpleQuickFixes/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9dfd9431a64258c6939f06c59d9163d33b65278cfd3ace4ae3c269f1e7e12d40 -size 6111 diff --git a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Dark.png new file mode 100644 index 0000000000000..524b085f8b0e6 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab8379f105e879912a52e2d25c88e26870df79bd220a6789a3f61e010d2f6788 +size 14944 diff --git a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Light.png b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Light.png new file mode 100644 index 0000000000000..cd0c2d36a627e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/GroupedCodeActions/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e641aa3e70e91cfbaf2db48fd1a095850a422b55b0acbeef4b2fdd74d07674e +size 14446 diff --git a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Dark.png new file mode 100644 index 0000000000000..7527ac7cf763e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94aa1fc6a75d1506459f1a0bfe4d29723350a59e3d4c5002a4916461245bcd6b +size 6517 diff --git a/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Light.png b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Light.png new file mode 100644 index 0000000000000..0de7a3d04abea --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/codeActionList/SimpleQuickFixes/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42e4676c9b49dec849b275057f4e8a06a3acbb7fb54304d9fb9509ff7277459d +size 6110 diff --git a/test/componentFixtures/.screenshots/baseline/codeEditor/CodeEditor/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Dark.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/codeEditor/CodeEditor/Dark.png rename to test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Dark.png diff --git a/test/componentFixtures/.screenshots/baseline/codeEditor/CodeEditor/Light.png b/test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Light.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/codeEditor/CodeEditor/Light.png rename to test/componentFixtures/.screenshots/baseline/editor/codeEditor/CodeEditor/Light.png diff --git a/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Dark.png new file mode 100644 index 0000000000000..48dbd2f491c97 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9df123fb04b4c6ecd4337bb19af8626b095c3fd5f149c0bcb2d46d216fa392ad +size 33087 diff --git a/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Light.png b/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Light.png new file mode 100644 index 0000000000000..2c81c27c7825a --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/findWidget/Find/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:531f9d16ecde8fc7b868eafc0541f04dfa476065095119ef2b02be2ee85a4788 +size 32732 diff --git a/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Dark.png new file mode 100644 index 0000000000000..027b13b8042f7 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa6ec0238ac1ac4974548b73e205b758e910d0604d1c28e0f26231e1df8129f2 +size 31006 diff --git a/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Light.png b/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Light.png new file mode 100644 index 0000000000000..82991aea52e6b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/findWidget/FindAndReplace/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:416cf9214bac2a0e275f9cf83c41e656e94de14ee870886f55caae230eb8871d +size 30907 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Dark.png new file mode 100644 index 0000000000000..d55a1ec7dfc84 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e8973161acbdd94816922272fcb0406263e58f624d86c0d7980c329a65a1b24 +size 11278 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Light.png new file mode 100644 index 0000000000000..5a87432a3d4ba --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/InsertionView/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6d3166596fd41609c4a622d7f7267fd6f3f9605d893efc34aac65ef7881ad71 +size 11203 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Dark.png new file mode 100644 index 0000000000000..9da6404a655ad --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f39335e6fa03078f804f848b634329c9391e4d8fe099d0fa1e0d4808ba4eb3a7 +size 11117 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Light.png new file mode 100644 index 0000000000000..42acebe80c019 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/SideBySideView/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c70bb66d053f20dccf9075399a801dceaa92d5830642e8f2afc9f89c81d9315c +size 11005 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Dark.png new file mode 100644 index 0000000000000..4f1806f3f3e06 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a54d85c0fcf622977f412e8fc2953c973f1da427c91d319d0708b6e758969f3f +size 10280 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Light.png new file mode 100644 index 0000000000000..ad3fa141565d5 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletions/WordReplacementView/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d494bbb9ebfbf2156c5d7a21f39c52e14313a5218af083cd91f2dbc6199f62da +size 10195 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Dark.png new file mode 100644 index 0000000000000..1dbc63aea4497 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e4226c71af8ae21a5f0c13ba521b50518afb4a39831e832d725ebe0289344ff +size 10266 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Light.png new file mode 100644 index 0000000000000..224da35d3dd1e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbar/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:518ecbddf013bbc749b62a8a74bad39d7683332ebe0d9f2a4b7b13ad54fcabab +size 10104 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Dark.png new file mode 100644 index 0000000000000..1dbc63aea4497 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e4226c71af8ae21a5f0c13ba521b50518afb4a39831e832d725ebe0289344ff +size 10266 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Light.png new file mode 100644 index 0000000000000..224da35d3dd1e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/HintsToolbarHovered/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:518ecbddf013bbc749b62a8a74bad39d7683332ebe0d9f2a4b7b13ad54fcabab +size 10104 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/JumpToHint/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Dark.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/JumpToHint/Dark.png rename to test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Dark.png diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/JumpToHint/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Light.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/JumpToHint/Light.png rename to test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/JumpToHint/Light.png diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Dark.png new file mode 100644 index 0000000000000..19c633f2edde5 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3923f85be8bcdbfcdecfd80b07abdbd71763edc379de4e6a5e946f20f160db5c +size 55650 diff --git a/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Light.png b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Light.png new file mode 100644 index 0000000000000..823489ce7fc5f --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/inlineCompletionsExtras/LongDistanceHint/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e81911488f22e9517b9e2f8bd7b8bc7a14d7fff1b2d012c2368b840d4bcb49cf +size 55028 diff --git a/test/componentFixtures/.screenshots/baseline/renameWidget/RenameClass/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Dark.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/renameWidget/RenameClass/Dark.png rename to test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Dark.png diff --git a/test/componentFixtures/.screenshots/baseline/renameWidget/RenameClass/Light.png b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Light.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/renameWidget/RenameClass/Light.png rename to test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameClass/Light.png diff --git a/test/componentFixtures/.screenshots/baseline/renameWidget/RenameVariable/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Dark.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/renameWidget/RenameVariable/Dark.png rename to test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Dark.png diff --git a/test/componentFixtures/.screenshots/baseline/renameWidget/RenameVariable/Light.png b/test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Light.png similarity index 100% rename from test/componentFixtures/.screenshots/baseline/renameWidget/RenameVariable/Light.png rename to test/componentFixtures/.screenshots/baseline/editor/renameWidget/RenameVariable/Light.png diff --git a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Dark.png new file mode 100644 index 0000000000000..cd464aa178c7e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b0b1f65ed8a0963da2b4f7f64b3d06683895a57d8101856f711742954cedc23 +size 23358 diff --git a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Light.png b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Light.png new file mode 100644 index 0000000000000..b293ccc538f56 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MethodCompletions/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2f348cbe0e5d401778f4f532b6254d981f9ac98220b290d8d5401dcd4526410 +size 22475 diff --git a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Dark.png b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Dark.png new file mode 100644 index 0000000000000..8d9e892d9e395 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0222974a073f5b12431b1ebc4f498ce8a795b860016520e103a35d6a68401d98 +size 13689 diff --git a/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Light.png b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Light.png new file mode 100644 index 0000000000000..5e93e621faf43 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/editor/suggestWidget/MixedKinds/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:998dde573af3df84cb836248c71f2bc141eb6845a7c407852f1a02969b14dec5 +size 13538 diff --git a/test/componentFixtures/.screenshots/baseline/findWidget/Find/Dark.png b/test/componentFixtures/.screenshots/baseline/findWidget/Find/Dark.png deleted file mode 100644 index 55c823622c787..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/findWidget/Find/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ed115cf70c25bc400d265a08db2e1d6a4d7596674d7a6ddc932ca1ad33ccaa25 -size 33367 diff --git a/test/componentFixtures/.screenshots/baseline/findWidget/Find/Light.png b/test/componentFixtures/.screenshots/baseline/findWidget/Find/Light.png deleted file mode 100644 index 9e80b63bb4819..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/findWidget/Find/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0cfebc3c9b7caa2d40cfb8db5b47ae6f5a446bb72883bdce5198ed5d296ae82f -size 32889 diff --git a/test/componentFixtures/.screenshots/baseline/findWidget/FindAndReplace/Dark.png b/test/componentFixtures/.screenshots/baseline/findWidget/FindAndReplace/Dark.png deleted file mode 100644 index cc9b2aa43769a..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/findWidget/FindAndReplace/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5d3a4d62c5226a9ca16b2327d95337a68286107a4fb12d1d5bc43530f17f6b1d -size 33379 diff --git a/test/componentFixtures/.screenshots/baseline/findWidget/FindAndReplace/Light.png b/test/componentFixtures/.screenshots/baseline/findWidget/FindAndReplace/Light.png deleted file mode 100644 index c8a7325251209..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/findWidget/FindAndReplace/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:522ba17bdd1d4378c0be262631d8e6bb1fd867f2999c07c06c4991cd9971a4cb -size 33114 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletions/InsertionView/Dark.png b/test/componentFixtures/.screenshots/baseline/inlineCompletions/InsertionView/Dark.png deleted file mode 100644 index ebee6a6096cdf..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletions/InsertionView/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0286377eace47ff7549c02aca2f00ad5c0200b29187ad187554b16304a93127c -size 11188 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletions/InsertionView/Light.png b/test/componentFixtures/.screenshots/baseline/inlineCompletions/InsertionView/Light.png deleted file mode 100644 index 63b05939a6dbd..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletions/InsertionView/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0af0112e3441d983dc0cde0a2bfa60542a32dfe552c1c377d8cf07c08b275429 -size 11067 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletions/SideBySideView/Dark.png b/test/componentFixtures/.screenshots/baseline/inlineCompletions/SideBySideView/Dark.png deleted file mode 100644 index ad2595b04837a..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletions/SideBySideView/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fc4b2b1a2972264878f5a4018c1030a2d1f34a5d5f468a1820f48cd041817421 -size 11036 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletions/SideBySideView/Light.png b/test/componentFixtures/.screenshots/baseline/inlineCompletions/SideBySideView/Light.png deleted file mode 100644 index 82b0147e7f2a8..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletions/SideBySideView/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1d8cb9f3a30edccfc6d304365b103c9787d2a1dbd2c7c8b011ef05d37825f326 -size 10906 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletions/WordReplacementView/Dark.png b/test/componentFixtures/.screenshots/baseline/inlineCompletions/WordReplacementView/Dark.png deleted file mode 100644 index 63033ca991851..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletions/WordReplacementView/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9b1e1b59a00dcea1e7f5f360907850f889959158f1ac32a7c276d771e2dea768 -size 10179 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletions/WordReplacementView/Light.png b/test/componentFixtures/.screenshots/baseline/inlineCompletions/WordReplacementView/Light.png deleted file mode 100644 index 4d303775e2646..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletions/WordReplacementView/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8438c8892a19b9b479e706efb002ec30c9b0c17cfd1575c0a4a883c6aba74e89 -size 10049 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbar/Dark.png b/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbar/Dark.png deleted file mode 100644 index 201fc1a7bf5ae..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbar/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a88fb3139a351b8c46df4309f64ed71210c456d4a89aad6b087ee512e26d9e1b -size 9762 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbar/Light.png b/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbar/Light.png deleted file mode 100644 index be9dca0e21be0..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbar/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d0679cbdb00bacb421b6c8c65b632211a5dd153ae4c698c7a49903e53632310f -size 9569 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbarHovered/Dark.png b/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbarHovered/Dark.png deleted file mode 100644 index 201fc1a7bf5ae..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbarHovered/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a88fb3139a351b8c46df4309f64ed71210c456d4a89aad6b087ee512e26d9e1b -size 9762 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbarHovered/Light.png b/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbarHovered/Light.png deleted file mode 100644 index be9dca0e21be0..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/HintsToolbarHovered/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d0679cbdb00bacb421b6c8c65b632211a5dd153ae4c698c7a49903e53632310f -size 9569 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/LongDistanceHint/Dark.png b/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/LongDistanceHint/Dark.png deleted file mode 100644 index 83d5938ca6c74..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/LongDistanceHint/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:05deae07ae1a6a5f738d6ebd4d475ed2e7d14630977b8b64cdae9030da2885ff -size 55645 diff --git a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/LongDistanceHint/Light.png b/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/LongDistanceHint/Light.png deleted file mode 100644 index 23db3df30fb8c..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/inlineCompletionsExtras/LongDistanceHint/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d9d81860fc8c296f9cc718ed3e6690669eb0f7301c17ae2f15639c984d499178 -size 54993 diff --git a/test/componentFixtures/.screenshots/baseline/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png b/test/componentFixtures/.screenshots/baseline/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png deleted file mode 100644 index dba2c92968eda..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/promptFilePickers/InstructionFilesWithAgentInstructions/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8c54a17b190dde0caa1f175b1ef025850a6ede44f03d8d124859355c309aa28a -size 27557 diff --git a/test/componentFixtures/.screenshots/baseline/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png b/test/componentFixtures/.screenshots/baseline/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png deleted file mode 100644 index 63742c15bcda1..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/promptFilePickers/InstructionFilesWithAgentInstructions/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eac6ff34521293e8f1f7e70f3f1e4227cc371861763fe954cce62d49b181955b -size 26846 diff --git a/test/componentFixtures/.screenshots/baseline/promptFilePickers/PromptFiles/Dark.png b/test/componentFixtures/.screenshots/baseline/promptFilePickers/PromptFiles/Dark.png deleted file mode 100644 index ffc64ba9aac75..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/promptFilePickers/PromptFiles/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:099e8f6e3b7c1c33fae6a75f47e387ab7f87536953607ec0f3a7b5160e0d4d59 -size 23377 diff --git a/test/componentFixtures/.screenshots/baseline/promptFilePickers/PromptFiles/Light.png b/test/componentFixtures/.screenshots/baseline/promptFilePickers/PromptFiles/Light.png deleted file mode 100644 index c9ced02aeb75c..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/promptFilePickers/PromptFiles/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0e7def912d49d13cd496386ef22a39df862a558e5585b01bfc46fd6f5f3276d4 -size 22969 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Dark.png new file mode 100644 index 0000000000000..f61177717733c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19c85e77d0f96df99aee39232dec5ae19f247c22f32664689794e3ca34c6ac4a +size 3899 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Light.png new file mode 100644 index 0000000000000..f8d4a437c6fa8 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/AvailableForDownload/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34f56e3559fc17204bd7e7134e8883e31cef957a6bc05780a929cf5a8e23071e +size 3830 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Dark.png new file mode 100644 index 0000000000000..586892704bbc6 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3da890451d346196f63be699e367ad2eccf46091167ac005d4eb5adb8169d12d +size 3778 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Light.png new file mode 100644 index 0000000000000..d5f5baa7a48b4 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/CheckingForUpdatesHidden/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc0b866a49984f2e17b2da44d15fa4525a21519010e4df5a0651ffcc88b594f0 +size 3674 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Dark.png new file mode 100644 index 0000000000000..a628d1aae9c23 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60be48fef4a63200229dda97c1f8664e9410d76be3a113926355867c8f58c3fa +size 3815 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Light.png new file mode 100644 index 0000000000000..0f6db97e2ab88 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/DownloadedInstalling/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44d8727fcac81f4669821e279f370cde522e2be40758d2d2975a9331466e6d49 +size 3716 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Dark.png new file mode 100644 index 0000000000000..f61177717733c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19c85e77d0f96df99aee39232dec5ae19f247c22f32664689794e3ca34c6ac4a +size 3899 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Light.png new file mode 100644 index 0000000000000..f8d4a437c6fa8 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Downloading30Percent/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34f56e3559fc17204bd7e7134e8883e31cef957a6bc05780a929cf5a8e23071e +size 3830 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Dark.png new file mode 100644 index 0000000000000..9c33813aae355 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c282cd99bdd694629caf8bd310013e3aec0f5b25dd5376ac7c1bb897a1b4388 +size 1593 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Light.png new file mode 100644 index 0000000000000..753352cf64f7d --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/LoadingSignedOutNoUpdate/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1380daf9f875ed2502b0669af7844bae000168bcba00d89ea5d6634f590637c1 +size 1606 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Dark.png new file mode 100644 index 0000000000000..f61177717733c --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19c85e77d0f96df99aee39232dec5ae19f247c22f32664689794e3ca34c6ac4a +size 3899 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Light.png new file mode 100644 index 0000000000000..f8d4a437c6fa8 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Overwriting/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34f56e3559fc17204bd7e7134e8883e31cef957a6bc05780a929cf5a8e23071e +size 3830 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Dark.png new file mode 100644 index 0000000000000..aa6921a198cc9 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:263cc0cd0dd5fe9eb7839cd0ff5c1698c26b76fd162c6f20cd9bf048094cb308 +size 4299 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Light.png new file mode 100644 index 0000000000000..7a19687be82cb --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Ready/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:651452ec039ff26c1fce42ccd541d0ce70f39a993457939be3bcd3f1d67ec4cb +size 4202 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Dark.png new file mode 100644 index 0000000000000..586892704bbc6 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3da890451d346196f63be699e367ad2eccf46091167ac005d4eb5adb8169d12d +size 3778 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Light.png new file mode 100644 index 0000000000000..d5f5baa7a48b4 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedInNoUpdate/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc0b866a49984f2e17b2da44d15fa4525a21519010e4df5a0651ffcc88b594f0 +size 3674 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Dark.png new file mode 100644 index 0000000000000..3dea19001f23b --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c6eba3c086a1b7c69b8f1bff63ccd43bbe6bad82a049e8b32230e4ce7ffde07 +size 1367 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Light.png new file mode 100644 index 0000000000000..3e8db51c45087 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/SignedOutNoUpdate/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b782263b3ffed17f79b95444cc6f479e9408ed252f5d1c227b4c7f741858b82 +size 1240 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Dark.png new file mode 100644 index 0000000000000..cca6f012d85f7 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa8e4d7c2bffaa4b3fd149ce1316b78ba9ac40ce5878a8867dd6422342c5a18f +size 3977 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Light.png new file mode 100644 index 0000000000000..c0152452dbfab --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/accountWidget/Updating/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13659a984dabca598789efd2b5a3f0753c5d8d8ba705b018c046ee1ebf9d861b +size 3855 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Dark.png new file mode 100644 index 0000000000000..dc686609d7ff1 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6332afcc844f37a800f11ba7b49f8d5ef61f160f9ea285a34fcf08df462c8de +size 1749 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Light.png new file mode 100644 index 0000000000000..dbca2be966bc8 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Collapsed/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c567d79fce21467c90bdf833d5d387c1a7e91070fa908d7fdb53595a0adfcb1 +size 1686 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Dark.png new file mode 100644 index 0000000000000..3c121b4057c08 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b217c46a4e2cfd993def8baf8e2fb2808a825d0b38fad553ec82a65ee1148e72 +size 1936 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Light.png new file mode 100644 index 0000000000000..08073d4119ac8 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/CollapsedWithMcpServers/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86dd22c02f0f4d1f4a46ce55f8546dfe70032a199ffa10683a9daa47cecfad59 +size 1889 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Dark.png new file mode 100644 index 0000000000000..162ecbb095203 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0bdc6ca3ffa8ea88ed6184379239b81d0ecd90cfc56dc3cecd84be616d3c22c0 +size 8122 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Light.png new file mode 100644 index 0000000000000..03dd01d13f420 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/Expanded/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5ff42ad20ea3c0fa7e43d1db6fac737890f149942caff51e3e0e1d041a3c015 +size 7865 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Dark.png new file mode 100644 index 0000000000000..2bebcf187a917 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2a797c09dcd81f2128eb937eb53f1f7a28131978a7f654dc95092b39172d098 +size 9388 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Light.png new file mode 100644 index 0000000000000..556520e8b8b97 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithCounts/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a26cc493b80a889baa2495307abd147405ef256c0713a84fcae26cb5918b39e4 +size 9131 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Dark.png new file mode 100644 index 0000000000000..621b8b35147ad --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b835e8c51e723ecb0a11cb167b81a76b4a4938895a1b05291ac5fd609446923 +size 8341 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Light.png new file mode 100644 index 0000000000000..b5d3ff1d64e8e --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/aiCustomizationShortcutsWidget/WithMcpServers/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae32840ac5637faa899f21c94acdc7e2f5c065d705e2a3b103a23a381849dfde +size 8065 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Dark.png new file mode 100644 index 0000000000000..d06f5a35857ed --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:125f1d570fdad186e94fc580d44e8560b6ecc3de58e3adc86d23045bdda21a8d +size 7784 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Light.png new file mode 100644 index 0000000000000..9ce0500c3bae3 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverAvailableForDownload/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:afb19ee8b136f65d80f8aa7de11cf983926458999b153568d6fe6303ab5c0236 +size 7692 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Dark.png new file mode 100644 index 0000000000000..01d70e70f0a70 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28bdc58d5f8ec2b28df3b6762fddb4dcc9477fd3dfe98af5a7d4c311ebf4e3c1 +size 7218 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Light.png new file mode 100644 index 0000000000000..4ffae5781dc67 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverDownloading30Percent/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:977c95e98e10ec21093424a2d4038131b9d95c69d27ef225b501529c4308e497 +size 7076 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Dark.png new file mode 100644 index 0000000000000..571d58ad4c3e7 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:595c42b464f87696ead04be8a367c1ba36dd75ec5115fa00ad7ee2ddc80c7644 +size 6875 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Light.png new file mode 100644 index 0000000000000..3c0f774ad1066 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverInstalling/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d393b6215ebc0f299a2a7e21076835c6b3ee9035b94917ddc5a7d298222c1b3 +size 6706 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Dark.png new file mode 100644 index 0000000000000..acbc58e86db97 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abdf653a0b09dd50655217b4c05e55cbb7a1f53ab8768809125d4eff48cace22 +size 7523 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Light.png new file mode 100644 index 0000000000000..7755b21fe51f1 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverReady/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:269f58f05dd9f4cea1b4f09a92f6f58b2cc55345e9252b05297b7328489dd0bf +size 7303 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Dark.png new file mode 100644 index 0000000000000..a574cc4e10157 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:631998b418a3af1a791470fb4b557e2b3fcd35d66d50e6c98e5c70d578b016cb +size 7367 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Light.png new file mode 100644 index 0000000000000..9f89acdebc18d --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverSameVersion/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f96bd7a669274b547229bce41ffc400983dc4656d97ecf820fc94d65df1a418 +size 7078 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Dark.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Dark.png new file mode 100644 index 0000000000000..1b1a0fe693f53 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e15ec5f380f9bc357fc6ddbd02b954930815bbdabd0d86c52b9e05550ad3a21c +size 7101 diff --git a/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Light.png b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Light.png new file mode 100644 index 0000000000000..0fd86f3732578 --- /dev/null +++ b/test/componentFixtures/.screenshots/baseline/sessions/updateHoverWidget/UpdateHoverUpdating/Light.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f50b8ee6944db9878190b9ec57fa87c4032d6697cb3827db3fa651f25aa38183 +size 6980 diff --git a/test/componentFixtures/.screenshots/baseline/suggestWidget/MethodCompletions/Dark.png b/test/componentFixtures/.screenshots/baseline/suggestWidget/MethodCompletions/Dark.png deleted file mode 100644 index 20494f462540f..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/suggestWidget/MethodCompletions/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:869d6db0575e1e0dce468df0227cd68c1725d7341c5efb5b47ab929d3717761d -size 22978 diff --git a/test/componentFixtures/.screenshots/baseline/suggestWidget/MethodCompletions/Light.png b/test/componentFixtures/.screenshots/baseline/suggestWidget/MethodCompletions/Light.png deleted file mode 100644 index aefc052d100ea..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/suggestWidget/MethodCompletions/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cff0734d5bbb48f8900e8cff6892aab851f58206db69a4c0a49425c0981f3967 -size 22208 diff --git a/test/componentFixtures/.screenshots/baseline/suggestWidget/MixedKinds/Dark.png b/test/componentFixtures/.screenshots/baseline/suggestWidget/MixedKinds/Dark.png deleted file mode 100644 index f44709ad5bfd2..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/suggestWidget/MixedKinds/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b84d2c246a9594012baff0a70212b6768e6b0d784936c4f2f9d37378cf813248 -size 13541 diff --git a/test/componentFixtures/.screenshots/baseline/suggestWidget/MixedKinds/Light.png b/test/componentFixtures/.screenshots/baseline/suggestWidget/MixedKinds/Light.png deleted file mode 100644 index 2f008a94f3e4b..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/suggestWidget/MixedKinds/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3c3aba099609c37eb2e58de791958665aec9fd8d2931b2208ec91515cac41b96 -size 13386 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/AvailableForDownload/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/AvailableForDownload/Dark.png deleted file mode 100644 index 4024eff5bc975..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/AvailableForDownload/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:63aa8f046ab23ac6bd53722db86fd254c7cd88a487f45feb68cc9a9bbd18300f -size 1209 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/AvailableForDownload/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/AvailableForDownload/Light.png deleted file mode 100644 index 5baee83328743..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/AvailableForDownload/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6d69bf362154015616fbb8c95052466061b7982f54203faea3ae89ad5afeaec3 -size 1221 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/CheckingForUpdates/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/CheckingForUpdates/Dark.png deleted file mode 100644 index 1197206909f45..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/CheckingForUpdates/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:43c9b8702922e8892a0cf20afc053e8ac56cdff65c02a9d47cb0183262da7bd4 -size 1908 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/CheckingForUpdates/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/CheckingForUpdates/Light.png deleted file mode 100644 index 11d36a2938778..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/CheckingForUpdates/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e0a17ec5b2f0e18be88383e491a09260654ce828236e37c11d5028e6ac326daa -size 1924 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloaded/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloaded/Dark.png deleted file mode 100644 index 0e6b503cc23c4..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloaded/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6897095d4c9d17a31bce693d2ce0f800d13ec56f2e27f4c5ec303ee64c7d19c3 -size 2189 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloaded/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloaded/Light.png deleted file mode 100644 index 91af710d8796d..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloaded/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3491ca579cf94b7148f4a943dc1e1a3eb44dd444d5268963c6a15a949d13bf88 -size 2056 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading0Percent/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading0Percent/Dark.png deleted file mode 100644 index db4f67e3b5d69..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading0Percent/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f3345ce1dc95c59d627fd618ddbf2f4028f0e7b13528bd3325fe6882cfb73891 -size 1781 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading0Percent/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading0Percent/Light.png deleted file mode 100644 index a59cf06f88803..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading0Percent/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7d4921395a33a571db185f7015160a23ee6e8ba3f82281a8ab4a935e43950b44 -size 1811 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading100Percent/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading100Percent/Dark.png deleted file mode 100644 index 28c4dcb54780c..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading100Percent/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a7bc3e108e03debccd5881474ff30f42bf5c56a43a7b4525d3d2fc16b075b56d -size 2482 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading100Percent/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading100Percent/Light.png deleted file mode 100644 index 118bab91cc1da..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading100Percent/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8df766b159576a0c12683af17f7d9ed391a2bd765743494f3760d8b415380c22 -size 2252 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading30Percent/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading30Percent/Dark.png deleted file mode 100644 index 9bb317db8757f..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading30Percent/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1c1f6f054881033c6162405b99e99ce8f049b9b20d320a74abc07793987474d9 -size 2249 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading30Percent/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading30Percent/Light.png deleted file mode 100644 index c97505c005c0f..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading30Percent/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4b6101c92d7073116c81eba73ddc08c721c2e947b4f7714887c54ffb91284d4e -size 2138 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading65Percent/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading65Percent/Dark.png deleted file mode 100644 index c1853513bec44..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading65Percent/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3179d62298aff75dfc01442a4589a61d83b6ba7f14c4b8441c9a688ca655738d -size 2434 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading65Percent/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading65Percent/Light.png deleted file mode 100644 index 8060149471e0d..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Downloading65Percent/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5e9dbcbf22233e5b48617067c8993a51c6b10afd940cd499270183e8eb4430f4 -size 2217 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/DownloadingIndeterminate/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/DownloadingIndeterminate/Dark.png deleted file mode 100644 index 4b0fc104ee8c5..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/DownloadingIndeterminate/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:13295a45a513349c0c8ae6129301d5f2fb89549cae3b4453c6962a770b6290ec -size 3716 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/DownloadingIndeterminate/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/DownloadingIndeterminate/Light.png deleted file mode 100644 index d7a897ebda5d6..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/DownloadingIndeterminate/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:21dbc34d1ec2776453bc176631f2a0e9a03f7fcd528369efcb40fd5c49536c92 -size 3837 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Overwriting/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Overwriting/Dark.png deleted file mode 100644 index db4f67e3b5d69..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Overwriting/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f3345ce1dc95c59d627fd618ddbf2f4028f0e7b13528bd3325fe6882cfb73891 -size 1781 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Overwriting/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Overwriting/Light.png deleted file mode 100644 index a59cf06f88803..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Overwriting/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7d4921395a33a571db185f7015160a23ee6e8ba3f82281a8ab4a935e43950b44 -size 1811 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Ready/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Ready/Dark.png deleted file mode 100644 index 017a1ebcebe3a..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Ready/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f97568f25aeafee2be621a345675fb29afe6c17a6655eeb2b053e09a671f2fe7 -size 2116 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Ready/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Ready/Light.png deleted file mode 100644 index 230402c3327fb..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Ready/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:faf0132e6298ffe9f285c4a2d6b7a0ef13c440175a22fa8febaff6b881cb4907 -size 1932 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Updating/Dark.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Updating/Dark.png deleted file mode 100644 index 4024eff5bc975..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Updating/Dark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:63aa8f046ab23ac6bd53722db86fd254c7cd88a487f45feb68cc9a9bbd18300f -size 1209 diff --git a/test/componentFixtures/.screenshots/baseline/updateWidget/Updating/Light.png b/test/componentFixtures/.screenshots/baseline/updateWidget/Updating/Light.png deleted file mode 100644 index 5baee83328743..0000000000000 --- a/test/componentFixtures/.screenshots/baseline/updateWidget/Updating/Light.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6d69bf362154015616fbb8c95052466061b7982f54203faea3ae89ad5afeaec3 -size 1221 diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index 6e7624dc0da22..f67a55707341c 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -22,9 +22,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "version": "1.19.10", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.10.tgz", + "integrity": "sha512-hZ7nOssGqRgyV3FVVQdfi+U4q02uB23bpnYpdvNXkYTRRyWx84b7yf1ans+dnJ/7h41sGL3CeQTfO+ZGxuO+Iw==", "license": "MIT", "engines": { "node": ">=18.14.1" diff --git a/test/monaco/package-lock.json b/test/monaco/package-lock.json index e1e44348099d6..45660fad3e849 100644 --- a/test/monaco/package-lock.json +++ b/test/monaco/package-lock.json @@ -1552,16 +1552,6 @@ "node": ">=6" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -1629,27 +1619,6 @@ "node": ">=8" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", @@ -1680,16 +1649,6 @@ "semver": "bin/semver.js" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -1836,16 +1795,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { diff --git a/test/sanity/package-lock.json b/test/sanity/package-lock.json index 79d178a5f5cbf..c441eb7cf188c 100644 --- a/test/sanity/package-lock.json +++ b/test/sanity/package-lock.json @@ -918,15 +918,6 @@ "node": ">=18" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -949,33 +940,13 @@ "node": ">=0.10.0" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", + "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/shebang-command": { diff --git a/test/sanity/package.json b/test/sanity/package.json index 0b281e7a2ca3c..93d586f98e0f1 100644 --- a/test/sanity/package.json +++ b/test/sanity/package.json @@ -20,5 +20,8 @@ "@types/mocha": "^10.0.10", "@types/node": "22.x", "typescript": "^6.0.0-dev.20251110" + }, + "overrides": { + "serialize-javascript": "^7.0.3" } } diff --git a/test/sanity/src/uiTest.ts b/test/sanity/src/uiTest.ts index 582c4eeab3b4b..65f8b14a49ea1 100644 --- a/test/sanity/src/uiTest.ts +++ b/test/sanity/src/uiTest.ts @@ -162,7 +162,7 @@ export class UITest { await installButton.click(); this.context.log('Waiting for extension to be installed'); - await page.locator('.extension-action:not(.disabled)', { hasText: /Uninstall/ }).waitFor({ timeout: 5 * 60_1000 }); + await page.getByRole('button', { name: 'Uninstall' }).first().waitFor({ timeout: 5 * 60_000 }); } /**