diff --git a/.github/agents/data.md b/.github/agents/data.md index 5809fb06d861a..605bd276ef9a3 100644 --- a/.github/agents/data.md +++ b/.github/agents/data.md @@ -1,8 +1,7 @@ --- name: Data description: Answer telemetry questions with data queries using Kusto Query Language (KQL) -tools: - ['vscode/extensions', 'execute/runInTerminal', 'read/readFile', 'search', 'web/githubRepo', 'azure-mcp/kusto_query', 'todo'] +tools: [vscode/extensions, execute/runInTerminal, read/readFile, search, azure-mcp/kusto_query, todo, ms-vscode.kusto-client/kusto, ms-vscode.kusto-client/kustoQueryExecution] --- # Role and Objective @@ -14,7 +13,9 @@ You are a Azure Data Explorer data analyst with expert knowledge in Kusto Query 1. Read `vscode-telemetry-docs/.github/copilot-instructions.md` to understand how to access VS Code's telemetry - If the `vscode-telemetry-docs` folder doesn't exist (just check your workspace_info, no extra tool call needed), run `npm run mixin-telemetry-docs` to clone the telemetry documentation. 2. Analyze data using kusto queries: Don't just describe what could be queried - actually execute Kusto queries to provide real data and insights: - - If the `kusto_query` tool doesn't exist (just check your provided tools, no need to run it!), install the `ms-azuretools.vscode-azure-mcp-server` VS Code extension + - You need either the **Kusto Explorer** extension (`ms-vscode.kusto-client`) or the **Azure MCP** extension (`ms-azuretools.vscode-azure-mcp-server`) installed to run queries. + - **Prefer Kusto Explorer** (`kusto_runQuery` / `kusto_checkQueryExecution` tools) over Azure MCP (`kusto_query` tool) when both are available. + - If neither tool is available (just check your provided tools, no need to run them!), install the Kusto Explorer extension (`ms-vscode.kusto-client`). If that is not an option, fall back to installing the Azure MCP extension (`ms-azuretools.vscode-azure-mcp-server`). - Use the appropriate Kusto cluster and database for the data type - Always include proper time filtering to limit data volume - Default to a rolling 28-day window if no specific timeframe is requested diff --git a/.vscode/settings.json b/.vscode/settings.json index 9587ec262cfc0..0a58b8a5bf849 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -93,7 +93,6 @@ "src/vs/editor/test/node/diffing/fixtures/**": true, "build/loader.min": true, "**/*.snap": true, - "src/vs/sessions/**": true }, // --- TypeScript --- "typescript.experimental.useTsgo": true, diff --git a/build/package-lock.json b/build/package-lock.json index 92ebc368df200..ffcaa1455e746 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -7945,9 +7945,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { diff --git a/extensions/media-preview/tsconfig.browser.json b/extensions/media-preview/tsconfig.browser.json index 3694afc77ee63..ea6be8e9a508f 100644 --- a/extensions/media-preview/tsconfig.browser.json +++ b/extensions/media-preview/tsconfig.browser.json @@ -1,3 +1,3 @@ { - "extends": "./tsconfig" + "extends": "./tsconfig.json" } diff --git a/extensions/merge-conflict/.vscodeignore b/extensions/merge-conflict/.vscodeignore index 3a8a2a96a6cd7..0628555db0021 100644 --- a/extensions/merge-conflict/.vscodeignore +++ b/extensions/merge-conflict/.vscodeignore @@ -1,6 +1,5 @@ src/** -tsconfig.json +tsconfig*.json out/** -extension.webpack.config.js -extension-browser.webpack.config.js +esbuild*.mts package-lock.json diff --git a/extensions/merge-conflict/esbuild.browser.mts b/extensions/merge-conflict/esbuild.browser.mts new file mode 100644 index 0000000000000..cb1249901465a --- /dev/null +++ b/extensions/merge-conflict/esbuild.browser.mts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.mts'; + +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist', 'browser'); + +run({ + platform: 'browser', + entryPoints: { + 'mergeConflictMain': path.join(srcDir, 'mergeConflictMain.ts'), + }, + srcDir, + outdir: outDir, + additionalOptions: { + tsconfig: path.join(import.meta.dirname, 'tsconfig.browser.json'), + } +}, process.argv); diff --git a/extensions/references-view/extension.webpack.config.js b/extensions/merge-conflict/esbuild.mts similarity index 50% rename from extensions/references-view/extension.webpack.config.js rename to extensions/merge-conflict/esbuild.mts index 4928186ae556c..4b450a4bafaa6 100644 --- a/extensions/references-view/extension.webpack.config.js +++ b/extensions/merge-conflict/esbuild.mts @@ -2,15 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check -import withDefaults from '../shared.webpack.config.mjs'; +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.mts'; -export default withDefaults({ - context: import.meta.dirname, - resolve: { - mainFields: ['module', 'main'] +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist'); + +run({ + platform: 'node', + entryPoints: { + 'mergeConflictMain': path.join(srcDir, 'mergeConflictMain.ts'), }, - entry: { - extension: './src/extension.ts', - } -}); + srcDir, + outdir: outDir, +}, process.argv); diff --git a/extensions/merge-conflict/package.json b/extensions/merge-conflict/package.json index 5b0eaa1b29c8f..42fb877b5763b 100644 --- a/extensions/merge-conflict/package.json +++ b/extensions/merge-conflict/package.json @@ -26,7 +26,13 @@ "browser": "./dist/browser/mergeConflictMain", "scripts": { "compile": "gulp compile-extension:merge-conflict", - "watch": "gulp watch-extension:merge-conflict" + "watch": "gulp watch-extension:merge-conflict", + "compile-web": "npm-run-all2 -lp bundle-web typecheck-web", + "bundle-web": "node ./esbuild.browser.mts", + "typecheck-web": "tsgo --project ./tsconfig.json --noEmit", + "watch-web": "npm-run-all2 -lp watch-bundle-web watch-typecheck-web", + "watch-bundle-web": "node ./esbuild.browser.mts --watch", + "watch-typecheck-web": "tsgo --project ./tsconfig.json --noEmit --watch" }, "contributes": { "commands": [ diff --git a/extensions/merge-conflict/tsconfig.browser.json b/extensions/merge-conflict/tsconfig.browser.json new file mode 100644 index 0000000000000..ea6be8e9a508f --- /dev/null +++ b/extensions/merge-conflict/tsconfig.browser.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig.json" +} diff --git a/extensions/mermaid-chat-features/tsconfig.browser.json b/extensions/mermaid-chat-features/tsconfig.browser.json index 3694afc77ee63..ea6be8e9a508f 100644 --- a/extensions/mermaid-chat-features/tsconfig.browser.json +++ b/extensions/mermaid-chat-features/tsconfig.browser.json @@ -1,3 +1,3 @@ { - "extends": "./tsconfig" + "extends": "./tsconfig.json" } diff --git a/extensions/references-view/.vscodeignore b/extensions/references-view/.vscodeignore index 4d2ffa699e4f4..4a97d0f9e7c4b 100644 --- a/extensions/references-view/.vscodeignore +++ b/extensions/references-view/.vscodeignore @@ -1,6 +1,6 @@ .vscode/** src/** out/** -tsconfig.json -*.webpack.config.js +tsconfig*.json +esbuild*.mts package-lock.json diff --git a/extensions/references-view/esbuild.browser.mts b/extensions/references-view/esbuild.browser.mts new file mode 100644 index 0000000000000..9ea4d5f68401e --- /dev/null +++ b/extensions/references-view/esbuild.browser.mts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.mts'; + +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist', 'browser'); + +run({ + platform: 'browser', + entryPoints: { + 'extension': path.join(srcDir, 'extension.ts'), + }, + srcDir, + outdir: outDir, + additionalOptions: { + tsconfig: path.join(import.meta.dirname, 'tsconfig.browser.json'), + }, +}, process.argv); diff --git a/extensions/merge-conflict/extension-browser.webpack.config.js b/extensions/references-view/esbuild.mts similarity index 51% rename from extensions/merge-conflict/extension-browser.webpack.config.js rename to extensions/references-view/esbuild.mts index 7054f22b86803..2b75ca703da06 100644 --- a/extensions/merge-conflict/extension-browser.webpack.config.js +++ b/extensions/references-view/esbuild.mts @@ -2,15 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check -import { browser as withBrowserDefaults } from '../shared.webpack.config.mjs'; +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.mts'; -export default withBrowserDefaults({ - context: import.meta.dirname, - entry: { - extension: './src/mergeConflictMain.ts' +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist'); + +run({ + platform: 'node', + entryPoints: { + 'extension': path.join(srcDir, 'extension.ts'), }, - output: { - filename: 'mergeConflictMain.js' - } -}); + srcDir, + outdir: outDir, +}, process.argv); diff --git a/extensions/references-view/package.json b/extensions/references-view/package.json index b5ac88950703d..ae87e5944dc5c 100644 --- a/extensions/references-view/package.json +++ b/extensions/references-view/package.json @@ -27,7 +27,7 @@ "onCommand:editor.action.showReferences" ], "main": "./out/extension", - "browser": "./dist/extension.js", + "browser": "./dist/browser/extension", "contributes": { "configuration": { "properties": { @@ -396,7 +396,13 @@ }, "scripts": { "compile": "npx gulp compile-extension:references-view", - "watch": "npx gulp watch-extension:references-view" + "watch": "npx gulp watch-extension:references-view", + "compile-web": "npm-run-all2 -lp bundle-web typecheck-web", + "bundle-web": "node ./esbuild.browser.mts", + "typecheck-web": "tsgo --project ./tsconfig.json --noEmit", + "watch-web": "npm-run-all2 -lp watch-bundle-web watch-typecheck-web", + "watch-bundle-web": "node ./esbuild.browser.mts --watch", + "watch-typecheck-web": "tsgo --project ./tsconfig.json --noEmit --watch" }, "devDependencies": { "@types/node": "22.x" diff --git a/extensions/references-view/tsconfig.browser.json b/extensions/references-view/tsconfig.browser.json new file mode 100644 index 0000000000000..ea6be8e9a508f --- /dev/null +++ b/extensions/references-view/tsconfig.browser.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig.json" +} diff --git a/extensions/search-result/.vscodeignore b/extensions/search-result/.vscodeignore index 35b808e16f7ee..50bbe59eb5209 100644 --- a/extensions/search-result/.vscodeignore +++ b/extensions/search-result/.vscodeignore @@ -1,7 +1,6 @@ src/** out/** -tsconfig.json -extension.webpack.config.js -extension-browser.webpack.config.js +tsconfig*.json +esbuild*.mts package-lock.json syntaxes/generateTMLanguage.js diff --git a/extensions/search-result/esbuild.browser.mts b/extensions/search-result/esbuild.browser.mts new file mode 100644 index 0000000000000..9ea4d5f68401e --- /dev/null +++ b/extensions/search-result/esbuild.browser.mts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.mts'; + +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist', 'browser'); + +run({ + platform: 'browser', + entryPoints: { + 'extension': path.join(srcDir, 'extension.ts'), + }, + srcDir, + outdir: outDir, + additionalOptions: { + tsconfig: path.join(import.meta.dirname, 'tsconfig.browser.json'), + }, +}, process.argv); diff --git a/extensions/merge-conflict/extension.webpack.config.js b/extensions/search-result/esbuild.mts similarity index 51% rename from extensions/merge-conflict/extension.webpack.config.js rename to extensions/search-result/esbuild.mts index c927dcaf3719e..2b75ca703da06 100644 --- a/extensions/merge-conflict/extension.webpack.config.js +++ b/extensions/search-result/esbuild.mts @@ -2,15 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check -import withDefaults from '../shared.webpack.config.mjs'; +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.mts'; -export default withDefaults({ - context: import.meta.dirname, - entry: { - extension: './src/mergeConflictMain.ts' - }, - output: { - filename: 'mergeConflictMain.js' +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist'); + +run({ + platform: 'node', + entryPoints: { + 'extension': path.join(srcDir, 'extension.ts'), }, -}); + srcDir, + outdir: outDir, +}, process.argv); diff --git a/extensions/search-result/extension-browser.webpack.config.js b/extensions/search-result/extension-browser.webpack.config.js deleted file mode 100644 index 1e0caad7e7297..0000000000000 --- a/extensions/search-result/extension-browser.webpack.config.js +++ /dev/null @@ -1,18 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check -import { browser as withBrowserDefaults } from '../shared.webpack.config.mjs'; -import path from 'path'; - -export default withBrowserDefaults({ - context: import.meta.dirname, - entry: { - extension: './src/extension.ts' - }, - output: { - filename: 'extension.js', - path: path.join(import.meta.dirname, 'dist') - } -}); diff --git a/extensions/search-result/extension.webpack.config.js b/extensions/search-result/extension.webpack.config.js deleted file mode 100644 index 4928186ae556c..0000000000000 --- a/extensions/search-result/extension.webpack.config.js +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check -import withDefaults from '../shared.webpack.config.mjs'; - -export default withDefaults({ - context: import.meta.dirname, - resolve: { - mainFields: ['module', 'main'] - }, - entry: { - extension: './src/extension.ts', - } -}); diff --git a/extensions/search-result/package.json b/extensions/search-result/package.json index bcd0656eb6147..5e6c74618e034 100644 --- a/extensions/search-result/package.json +++ b/extensions/search-result/package.json @@ -10,13 +10,19 @@ "vscode": "^1.39.0" }, "main": "./out/extension.js", - "browser": "./dist/extension.js", + "browser": "./dist/browser/extension", "activationEvents": [ "onLanguage:search-result" ], "scripts": { "generate-grammar": "node ./syntaxes/generateTMLanguage.js", - "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:search-result ./tsconfig.json" + "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:search-result ./tsconfig.json", + "compile-web": "npm-run-all2 -lp bundle-web typecheck-web", + "bundle-web": "node ./esbuild.browser.mts", + "typecheck-web": "tsgo --project ./tsconfig.json --noEmit", + "watch-web": "npm-run-all2 -lp watch-bundle-web watch-typecheck-web", + "watch-bundle-web": "node ./esbuild.browser.mts --watch", + "watch-typecheck-web": "tsgo --project ./tsconfig.json --noEmit --watch" }, "capabilities": { "virtualWorkspaces": true, diff --git a/extensions/search-result/tsconfig.browser.json b/extensions/search-result/tsconfig.browser.json new file mode 100644 index 0000000000000..ea6be8e9a508f --- /dev/null +++ b/extensions/search-result/tsconfig.browser.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig.json" +} diff --git a/extensions/simple-browser/.vscodeignore b/extensions/simple-browser/.vscodeignore index ef5dc0365fa33..e2740fcfba08f 100644 --- a/extensions/simple-browser/.vscodeignore +++ b/extensions/simple-browser/.vscodeignore @@ -1,14 +1,11 @@ test/** test-workspace/** src/** -tsconfig.json +tsconfig*.json out/test/** out/** -extension.webpack.config.js -extension-browser.webpack.config.js +esbuild*.mts cgmanifest.json .gitignore package-lock.json preview-src/** -webpack.config.js -esbuild-* diff --git a/extensions/simple-browser/esbuild.browser.mts b/extensions/simple-browser/esbuild.browser.mts new file mode 100644 index 0000000000000..9ea4d5f68401e --- /dev/null +++ b/extensions/simple-browser/esbuild.browser.mts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.mts'; + +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist', 'browser'); + +run({ + platform: 'browser', + entryPoints: { + 'extension': path.join(srcDir, 'extension.ts'), + }, + srcDir, + outdir: outDir, + additionalOptions: { + tsconfig: path.join(import.meta.dirname, 'tsconfig.browser.json'), + }, +}, process.argv); diff --git a/extensions/references-view/extension-browser.webpack.config.js b/extensions/simple-browser/esbuild.mts similarity index 51% rename from extensions/references-view/extension-browser.webpack.config.js rename to extensions/simple-browser/esbuild.mts index 1e0caad7e7297..2b75ca703da06 100644 --- a/extensions/references-view/extension-browser.webpack.config.js +++ b/extensions/simple-browser/esbuild.mts @@ -2,17 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check -import { browser as withBrowserDefaults } from '../shared.webpack.config.mjs'; -import path from 'path'; +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.mts'; -export default withBrowserDefaults({ - context: import.meta.dirname, - entry: { - extension: './src/extension.ts' +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist'); + +run({ + platform: 'node', + entryPoints: { + 'extension': path.join(srcDir, 'extension.ts'), }, - output: { - filename: 'extension.js', - path: path.join(import.meta.dirname, 'dist') - } -}); + srcDir, + outdir: outDir, +}, process.argv); diff --git a/extensions/simple-browser/extension-browser.webpack.config.js b/extensions/simple-browser/extension-browser.webpack.config.js deleted file mode 100644 index b758f2d8155a3..0000000000000 --- a/extensions/simple-browser/extension-browser.webpack.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check -import { browser as withBrowserDefaults } from '../shared.webpack.config.mjs'; - -export default withBrowserDefaults({ - context: import.meta.dirname, - entry: { - extension: './src/extension.ts' - } -}); diff --git a/extensions/simple-browser/extension.webpack.config.js b/extensions/simple-browser/extension.webpack.config.js deleted file mode 100644 index 4928186ae556c..0000000000000 --- a/extensions/simple-browser/extension.webpack.config.js +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check -import withDefaults from '../shared.webpack.config.mjs'; - -export default withDefaults({ - context: import.meta.dirname, - resolve: { - mainFields: ['module', 'main'] - }, - entry: { - extension: './src/extension.ts', - } -}); diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index 691e72f248c41..18bd0027fdc59 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -72,8 +72,12 @@ "vscode:prepublish": "npm run build-ext && npm run build-webview", "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:simple-browser ./tsconfig.json", "build-webview": "node ./esbuild.webview.mts", - "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", - "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" + "compile-web": "npm-run-all2 -lp bundle-web typecheck-web", + "bundle-web": "node ./esbuild.browser.mts", + "typecheck-web": "tsgo --project ./tsconfig.json --noEmit", + "watch-web": "npm-run-all2 -lp watch-bundle-web watch-typecheck-web", + "watch-bundle-web": "node ./esbuild.browser.mts --watch", + "watch-typecheck-web": "tsgo --project ./tsconfig.json --noEmit --watch" }, "dependencies": { "@vscode/extension-telemetry": "^0.9.8" diff --git a/extensions/simple-browser/tsconfig.browser.json b/extensions/simple-browser/tsconfig.browser.json new file mode 100644 index 0000000000000..ea6be8e9a508f --- /dev/null +++ b/extensions/simple-browser/tsconfig.browser.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig.json" +} diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 2ea5212320763..4f25b3268fc3e 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -7,7 +7,7 @@ "foreground": "#bfbfbf", "disabledForeground": "#666666", "errorForeground": "#f48771", - "descriptionForeground": "#999999", + "descriptionForeground": "#888888", "icon.foreground": "#888888", "focusBorder": "#3994BCB3", "textBlockQuote.background": "#242526", @@ -53,7 +53,7 @@ "badge.background": "#3994BCF0", "badge.foreground": "#FFFFFF", "progressBar.background": "#878889", - "list.activeSelectionBackground": "#3994BC55", + "list.activeSelectionBackground": "#3994BC26", "list.activeSelectionForeground": "#bfbfbf", "list.inactiveSelectionBackground": "#2C2D2E", "list.inactiveSelectionForeground": "#bfbfbf", diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css index 8ea22bafb03a1..6dfbb2888a496 100644 --- a/extensions/theme-2026/themes/styles.css +++ b/extensions/theme-2026/themes/styles.css @@ -242,16 +242,16 @@ /* Chat Widget */ .monaco-workbench .interactive-session .chat-input-container { box-shadow: inset var(--shadow-sm); - border-radius: var(--radius-md); + border-radius: var(--radius-lg); } .monaco-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor, .monaco-workbench .interactive-session .chat-editing-session .chat-editing-session-container { - border-radius: var(--radius-sm) var(--radius-sm) 0 0; + border-radius: var(--radius-lg) var(--radius-lg) 0 0; } .monaco-workbench .interactive-input-part:has(.chat-editing-session > .chat-editing-session-container) .chat-input-container { - border-radius: 0 0 var(--radius-md) var(--radius-md); + border-radius: 0 0 var(--radius-lg) var(--radius-lg); } .monaco-workbench .part.panel .interactive-session, diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index fa278c0827683..5b64a6cbb1d83 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -1288,8 +1288,8 @@ export class CodeApplication extends Disposable { const context = isLaunchedFromCli(process.env) ? OpenContext.CLI : OpenContext.DESKTOP; const args = this.environmentMainService.args; - // Open sessions window if requested - if ((process as INodeProcess).isEmbeddedApp || args['sessions']) { + // Embedded app launches directly into the sessions window + if ((process as INodeProcess).isEmbeddedApp) { return windowsMainService.openSessionsWindow({ context, contextWindowId: undefined }); } diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index e2aa3084c3f14..8e29f4924766b 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ChildProcess, spawn, SpawnOptions, StdioOptions } from 'child_process'; -import { chmodSync, existsSync, readFileSync, readdirSync, statSync, truncateSync, unlinkSync } from 'fs'; +import { chmodSync, existsSync, readFileSync, statSync, truncateSync, unlinkSync } from 'fs'; import { homedir, tmpdir } from 'os'; import type { ProfilingSession, Target } from 'v8-inspect-profiler'; import { Event } from '../../base/common/event.js'; @@ -500,23 +500,7 @@ export async function main(argv: string[]): Promise { // focusing issues when the new instance only sends data to a previous instance and then closes. const spawnArgs = ['-n', '-g']; // -a opens the given application. - let appToLaunch = process.execPath; - if (args['sessions']) { - // process.execPath is e.g. /Applications/Code.app/Contents/MacOS/Electron - // Embedded app is at /Applications/Code.app/Contents/Applications/.app - const contentsPath = dirname(dirname(process.execPath)); - const applicationsPath = join(contentsPath, 'Applications'); - const embeddedApp = existsSync(applicationsPath) && readdirSync(applicationsPath).find(f => f.endsWith('.app')); - if (embeddedApp) { - appToLaunch = join(applicationsPath, embeddedApp); - argv = argv.filter(arg => arg !== '--sessions'); - } else { - console.error(`No embedded app found in: ${applicationsPath}`); - console.error('The --sessions flag requires an embedded app to be installed.'); - return; - } - } - spawnArgs.push('-a', appToLaunch); + spawnArgs.push('-a', process.execPath); // -a: opens a specific application if (args.verbose || args.status) { spawnArgs.push('--wait-apps'); // `open --wait-apps`: blocks until the launched app is closed (even if they were already running) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 3c12b59418e0e..2a452609df613 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -267,8 +267,8 @@ export class ActionList extends Disposable { private readonly _list: List>; - private readonly _actionLineHeight = 28; - private readonly _headerLineHeight = 28; + private readonly _actionLineHeight = 24; + private readonly _headerLineHeight = 24; private readonly _separatorLineHeight = 8; private readonly _allMenuItems: readonly IActionListItem[]; @@ -411,10 +411,18 @@ export class ActionList extends Disposable { focusPrevious() { this._list.focusPrevious(1, true, undefined, this.focusCondition); + const focused = this._list.getFocus(); + if (focused.length > 0) { + this._list.reveal(focused[0]); + } } focusNext() { this._list.focusNext(1, true, undefined, this.focusCondition); + const focused = this._list.getFocus(); + if (focused.length > 0) { + this._list.reveal(focused[0]); + } } acceptSelected(preview?: boolean) { diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index 0c63d24728647..4ea3a49bff7db 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -56,12 +56,12 @@ /** Styles for each row in the list element **/ .action-widget .monaco-list .monaco-list-row { - padding: 0 4px 0 4px; + padding: 0 4px 0 8px; white-space: nowrap; cursor: pointer; touch-action: none; width: 100%; - border-radius: 3px; + border-radius: var(--vscode-cornerRadius-small); } .action-widget .monaco-list .monaco-list-row.action.focused:not(.option-disabled) { @@ -73,8 +73,8 @@ .action-widget .monaco-list-row.group-header { color: var(--vscode-descriptionForeground) !important; - font-weight: 600; - font-size: 13px; + font-weight: 500; + font-size: 11px; } .action-widget .monaco-list-row.group-header:not(:first-of-type) { @@ -120,10 +120,18 @@ .action-widget .monaco-list-row.action { display: flex; - gap: 4px; + gap: 6px; align-items: center; } +.action-widget .monaco-list-row.action .codicon { + font-size: 12px; +} + +.action-widget .monaco-list-row.action .action-list-item-toolbar .codicon { + font-size: 16px; +} + .action-widget .monaco-list-row.action.option-disabled, .action-widget .monaco-list:focus .monaco-list-row.focused.action.option-disabled, .action-widget .monaco-list-row.action.option-disabled .codicon, @@ -168,13 +176,13 @@ } .action-widget .action-widget-action-bar .actions-container { - padding: 4px 8px 2px 24px; + padding: 2px 8px 0px 26px; width: auto; } .action-widget-action-bar .action-label { color: var(--vscode-textLink-activeForeground); - font-size: 13px; + font-size: 12px; line-height: 22px; padding: 0; pointer-events: all; diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 45cb23ba6f3f7..a10f4c9b3bbc5 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -53,7 +53,6 @@ export interface NativeParsedArgs { goto?: boolean; 'new-window'?: boolean; 'reuse-window'?: boolean; - 'sessions'?: boolean; locale?: string; 'user-data-dir'?: string; 'prof-startup'?: boolean; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 6d00ad0ae0908..35a833d5f903d 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -99,7 +99,6 @@ export const OPTIONS: OptionDescriptions> = { 'goto': { type: 'boolean', cat: 'o', alias: 'g', args: 'file:line[:character]', description: localize('goto', "Open a file at the path on the specified line and character position.") }, 'new-window': { type: 'boolean', cat: 'o', alias: 'n', description: localize('newWindow', "Force to open a new window.") }, 'reuse-window': { type: 'boolean', cat: 'o', alias: 'r', description: localize('reuseWindow', "Force to open a file or folder in an already opened window.") }, - 'sessions': { type: 'boolean', cat: 'o', description: localize('sessions', "Opens the sessions window.") }, 'wait': { type: 'boolean', cat: 'o', alias: 'w', description: localize('wait', "Wait for the files to be closed before returning.") }, 'waitMarkerFilePath': { type: 'string' }, 'locale': { type: 'string', cat: 'o', args: 'locale', description: localize('locale', "The locale to use (e.g. en-US or zh-TW).") }, diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 4431f19b65917..18c7915e8f24a 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -339,7 +339,7 @@ export interface IPtyService { shutdown(id: number, immediate: boolean): Promise; input(id: number, data: string): Promise; sendSignal(id: number, signal: string): Promise; - resize(id: number, cols: number, rows: number): Promise; + resize(id: number, cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): Promise; clearBuffer(id: number): Promise; getInitialCwd(id: number): Promise; getCwd(id: number): Promise; @@ -391,7 +391,7 @@ export interface IPtyServiceContribution { handleProcessReady(persistentProcessId: number, process: ITerminalChildProcess): void; handleProcessDispose(persistentProcessId: number): void; handleProcessInput(persistentProcessId: number, data: string): void; - handleProcessResize(persistentProcessId: number, cols: number, rows: number): void; + handleProcessResize(persistentProcessId: number, cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): void; } export interface IPtyHostController { @@ -810,7 +810,7 @@ export interface ITerminalChildProcess { input(data: string): void; sendSignal(signal: string): void; processBinary(data: string): Promise; - resize(cols: number, rows: number): void; + resize(cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): void; clearBuffer(): void | Promise; /** diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index d85ba59ef1242..0e66435f1ef7f 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -254,8 +254,8 @@ export class PtyHostService extends Disposable implements IPtyHostService { processBinary(id: number, data: string): Promise { return this._proxy.processBinary(id, data); } - resize(id: number, cols: number, rows: number): Promise { - return this._proxy.resize(id, cols, rows); + resize(id: number, cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): Promise { + return this._proxy.resize(id, cols, rows, pixelWidth, pixelHeight); } clearBuffer(id: number): Promise { return this._proxy.clearBuffer(id); diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 826b09f47c81d..e73086df28c9e 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -455,13 +455,13 @@ export class PtyService extends Disposable implements IPtyService { return this._throwIfNoPty(id).writeBinary(data); } @traceRpc - async resize(id: number, cols: number, rows: number): Promise { + async resize(id: number, cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): Promise { const pty = this._throwIfNoPty(id); if (pty) { for (const contrib of this._contributions) { - contrib.handleProcessResize(id, cols, rows); + contrib.handleProcessResize(id, cols, rows, pixelWidth, pixelHeight); } - pty.resize(cols, rows); + pty.resize(cols, rows, pixelWidth, pixelHeight); } } @traceRpc @@ -902,7 +902,7 @@ class PersistentTerminalProcess extends Disposable { writeBinary(data: string): Promise { return this._terminalProcess.processBinary(data); } - resize(cols: number, rows: number): void { + resize(cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): void { if (this._inReplay) { return; } @@ -911,7 +911,7 @@ class PersistentTerminalProcess extends Disposable { // Buffered events should flush when a resize occurs this._bufferer.flushBuffer(this._persistentProcessId); - return this._terminalProcess.resize(cols, rows); + return this._terminalProcess.resize(cols, rows, pixelWidth, pixelHeight); } async clearBuffer(): Promise { this._serializer.clearBuffer(); diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 707bfa7fc0ff7..1f49fa67daa7f 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -515,7 +515,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } } - resize(cols: number, rows: number): void { + resize(cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): void { if (this._store.isDisposed) { return; } @@ -537,7 +537,10 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._logService.trace('node-pty.IPty#resize', cols, rows); try { - this._ptyProcess.resize(cols, rows); + const pixelSize = pixelWidth !== undefined && pixelHeight !== undefined + ? { width: pixelWidth, height: pixelHeight } + : undefined; + this._ptyProcess.resize(cols, rows, pixelSize); } catch (e) { // Swallow error if the pty has already exited this._logService.trace('node-pty.IPty#resize exception ' + e.message); diff --git a/src/vs/sessions/LAYOUT.md b/src/vs/sessions/LAYOUT.md index 17ff3fee470a1..1448a8a3dca49 100644 --- a/src/vs/sessions/LAYOUT.md +++ b/src/vs/sessions/LAYOUT.md @@ -704,6 +704,7 @@ interface IPartVisibilityState { | Date | Change | |------|--------| +| 2026-02-17 | Added `-webkit-app-region: drag` to sidebar title area so it can be used to drag the window; interactive children (actions, composite bar, labels) marked `no-drag`; CSS rules scoped to `.agent-sessions-workbench` in `parts/media/sidebarPart.css` | | 2026-02-13 | Documentation sync: Updated all file names, class names, and references to match current implementation. `AgenticWorkbench` → `Workbench`, `AgenticSidebarPart` → `SidebarPart`, `AgenticAuxiliaryBarPart` → `AuxiliaryBarPart`, `AgenticPanelPart` → `PanelPart`, `agenticWorkbench.ts` → `workbench.ts`, `agenticWorkbenchMenus.ts` → `menus.ts`, `agenticLayoutActions.ts` → `layoutActions.ts`, `AgenticTitleBarWidget` → `SessionsTitleBarWidget`, `AgenticTitleBarContribution` → `SessionsTitleBarContribution`. Removed references to deleted files (`sidebarRevealButton.ts`, `floatingToolbar.ts`, `agentic.contributions.ts`, `agenticTitleBarWidget.ts`). Updated pane composite architecture from `SyncDescriptor`-based to `AgenticPaneCompositePartService`. Moved account widget docs from titlebar to sidebar footer. Added documentation for sidebar footer, project bar, traffic light spacer, card appearance styling, widget directory, and new contrib structure (`accountMenu/`, `chat/`, `configuration/`, `sessions/`). Updated titlebar actions to reflect Run Script split button and Open submenu. Removed Toggle Maximize panel action (no longer registered). Updated contributions section with all current contributions and their locations. | | 2026-02-13 | Changed grid structure: sidebar now spans full window height at root level (HORIZONTAL root orientation); Titlebar moved inside right section; Grid is now `Sidebar \| [Titlebar / TopRight / Panel]` instead of `Titlebar / [Sidebar \| RightSection]`; Panel maximize now excludes both titlebar and sidebar; Floating toolbar positioning no longer depends on titlebar height | | 2026-02-11 | Simplified titlebar: replaced `BrowserTitlebarPart`-derived implementation with standalone `TitlebarPart` using three `MenuWorkbenchToolBar` sections (left/center/right); Removed `CommandCenterControl`, `WindowTitle`, layout toolbar, and manual toolbar management; Center section uses `Menus.CommandCenter` which renders session picker via `IActionViewItemService`; Right section uses `Menus.TitleBarRight` which includes account submenu; Removed `commandCenterControl.ts` file | diff --git a/src/vs/sessions/browser/parts/media/sidebarPart.css b/src/vs/sessions/browser/parts/media/sidebarPart.css index 1c845edee3ba1..d8f4c72894978 100644 --- a/src/vs/sessions/browser/parts/media/sidebarPart.css +++ b/src/vs/sessions/browser/parts/media/sidebarPart.css @@ -8,6 +8,26 @@ display: none; } +/* Make the sidebar title area draggable to move the window */ +.agent-sessions-workbench .part.sidebar > .composite.title { + position: relative; +} + +.agent-sessions-workbench .part.sidebar > .composite.title > .titlebar-drag-region { + top: 0; + left: 0; + display: block; + position: absolute; + width: 100%; + height: 100%; + -webkit-app-region: drag; +} + +/* Interactive elements in the title area must not be draggable */ +.agent-sessions-workbench .part.sidebar > .composite.title .action-item { + -webkit-app-region: no-drag; +} + /* Sidebar Footer Container */ .monaco-workbench .part.sidebar > .sidebar-footer { display: flex; diff --git a/src/vs/sessions/browser/parts/sidebarPart.ts b/src/vs/sessions/browser/parts/sidebarPart.ts index fcae124b6e1ec..b689ed528f40f 100644 --- a/src/vs/sessions/browser/parts/sidebarPart.ts +++ b/src/vs/sessions/browser/parts/sidebarPart.ts @@ -134,6 +134,12 @@ export class SidebarPart extends AbstractPaneCompositePart { protected override createTitleArea(parent: HTMLElement): HTMLElement | undefined { const titleArea = super.createTitleArea(parent); + if (titleArea) { + // Add a drag region so the sidebar title area can be used to move the window, + // matching the titlebar's drag behavior. + prepend(titleArea, $('div.titlebar-drag-region')); + } + // macOS native: the sidebar spans full height and the traffic lights // overlay the top-left corner. Add a fixed-width spacer inside the // title area to push content horizontally past the traffic lights. diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationListWidget.ts b/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationListWidget.ts index 2b87f4219fa60..9c05cd61f7694 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationListWidget.ts +++ b/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationListWidget.ts @@ -39,7 +39,7 @@ import { ILabelService } from '../../../../platform/label/common/label.js'; import { parseAllHookFiles } from '../../../../workbench/contrib/chat/browser/promptSyntax/hookUtils.js'; import { OS } from '../../../../base/common/platform.js'; import { IRemoteAgentService } from '../../../../workbench/services/remote/common/remoteAgentService.js'; -import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js'; +import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { Action, Separator } from '../../../../base/common/actions.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; @@ -340,7 +340,7 @@ export class AICustomizationListWidget extends Disposable { @IPathService private readonly pathService: IPathService, @ILabelService private readonly labelService: ILabelService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, - @ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService, + @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, @ILogService private readonly logService: ILogService, @IClipboardService private readonly clipboardService: IClipboardService, @ISCMService private readonly scmService: ISCMService, diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagement.ts b/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagement.ts index e087542891a29..131fbeff9e96d 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagement.ts +++ b/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagement.ts @@ -5,7 +5,7 @@ import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { URI } from '../../../../base/common/uri.js'; -import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js'; +import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { localize } from '../../../../nls.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; @@ -96,7 +96,7 @@ export const SIDEBAR_MIN_WIDTH = 150; export const SIDEBAR_MAX_WIDTH = 350; export const CONTENT_MIN_WIDTH = 400; -export function getActiveSessionRoot(activeSessionService: ISessionsWorkbenchService): URI | undefined { +export function getActiveSessionRoot(activeSessionService: ISessionsManagementService): URI | undefined { const session = activeSessionService.getActiveSession(); return session?.worktree ?? session?.repository; } diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts b/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts index 10fad486f0ca8..4571e7b0e0eca 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts +++ b/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationManagementEditor.ts @@ -59,7 +59,7 @@ import { showConfigureHooksQuickPick } from '../../../../workbench/contrib/chat/ import { CustomizationCreatorService } from './customizationCreatorService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; -import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js'; +import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { IWorkingCopyService } from '../../../../workbench/services/workingCopy/common/workingCopyService.js'; @@ -176,7 +176,7 @@ export class AICustomizationManagementEditor extends EditorPane { @IConfigurationService private readonly configurationService: IConfigurationService, @ILayoutService private readonly layoutService: ILayoutService, @ICommandService private readonly commandService: ICommandService, - @ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService, + @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, ) { diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationOverviewView.ts b/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationOverviewView.ts index d6681398747d9..677fdf24320b7 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationOverviewView.ts +++ b/src/vs/sessions/contrib/aiCustomizationManagement/browser/aiCustomizationOverviewView.ts @@ -27,7 +27,7 @@ import { AICustomizationManagementEditorInput } from './aiCustomizationManagemen import { AICustomizationManagementEditor } from './aiCustomizationManagementEditor.js'; import { agentIcon, instructionsIcon, promptIcon, skillIcon } from '../../aiCustomizationTreeView/browser/aiCustomizationTreeViewIcons.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js'; +import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; const $ = DOM.$; @@ -66,7 +66,7 @@ export class AICustomizationOverviewView extends ViewPane { @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @IPromptsService private readonly promptsService: IPromptsService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService, + @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); diff --git a/src/vs/sessions/contrib/aiCustomizationManagement/browser/customizationCreatorService.ts b/src/vs/sessions/contrib/aiCustomizationManagement/browser/customizationCreatorService.ts index 271a1f94d0607..e1e8717e6afaa 100644 --- a/src/vs/sessions/contrib/aiCustomizationManagement/browser/customizationCreatorService.ts +++ b/src/vs/sessions/contrib/aiCustomizationManagement/browser/customizationCreatorService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js'; +import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { IChatWidgetService } from '../../../../workbench/contrib/chat/browser/chat.js'; import { IChatService } from '../../../../workbench/contrib/chat/common/chatService/chatService.js'; import { ChatModeKind } from '../../../../workbench/contrib/chat/common/constants.js'; @@ -30,7 +30,7 @@ export class CustomizationCreatorService { @ICommandService private readonly commandService: ICommandService, @IChatService private readonly chatService: IChatService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService, + @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, @IPromptsService private readonly promptsService: IPromptsService, @IQuickInputService private readonly quickInputService: IQuickInputService, ) { } diff --git a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts index b8948468fea04..b24b57c831fb0 100644 --- a/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts +++ b/src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeViewViews.ts @@ -35,7 +35,7 @@ import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js'; +import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; //#region Context Keys @@ -487,7 +487,7 @@ export class AICustomizationViewPane extends ViewPane { @IMenuService private readonly menuService: IMenuService, @ILogService private readonly logService: ILogService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService, + @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); diff --git a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts index 8d3344be48df0..afabe96726c78 100644 --- a/src/vs/sessions/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/sessions/contrib/chat/browser/chat.contribution.ts @@ -14,7 +14,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; import { isAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; -import { ISessionsWorkbenchService, IsNewChatSessionContext } from '../../sessions/browser/sessionsWorkbenchService.js'; +import { ISessionsManagementService, IsNewChatSessionContext } from '../../sessions/browser/sessionsManagementService.js'; import { ITerminalService, ITerminalGroupService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; import { Menus } from '../../../browser/menus.js'; import { BranchChatSessionAction } from './branchChatSessionAction.js'; @@ -46,9 +46,9 @@ export class OpenSessionWorktreeInVSCodeAction extends Action2 { override async run(accessor: ServicesAccessor,): Promise { const hostService = accessor.get(IHostService); - const agentSessionsService = accessor.get(ISessionsWorkbenchService); + const sessionsManagementService = accessor.get(ISessionsManagementService); - const activeSession = agentSessionsService.activeSession.get(); + const activeSession = sessionsManagementService.activeSession.get(); if (!activeSession) { return; } @@ -82,9 +82,9 @@ export class OpenSessionInTerminalAction extends Action2 { override async run(accessor: ServicesAccessor,): Promise { const terminalService = accessor.get(ITerminalService); const terminalGroupService = accessor.get(ITerminalGroupService); - const agentSessionsService = accessor.get(ISessionsWorkbenchService); + const sessionsManagementService = accessor.get(ISessionsManagementService); - const activeSession = agentSessionsService.activeSession.get(); + const activeSession = sessionsManagementService.activeSession.get(); const repository = isAgentSession(activeSession) && activeSession.providerType !== AgentSessionProviders.Cloud ? activeSession.worktree : undefined; diff --git a/src/vs/sessions/contrib/chat/browser/media/chatWidget.css b/src/vs/sessions/contrib/chat/browser/media/chatWidget.css index 83bce416db037..e3d76a95b72c6 100644 --- a/src/vs/sessions/contrib/chat/browser/media/chatWidget.css +++ b/src/vs/sessions/contrib/chat/browser/media/chatWidget.css @@ -36,7 +36,7 @@ /* Editor */ .sessions-chat-editor { - padding: 0 10px; + padding: 0 6px 6px 6px; height: 72px; min-height: 72px; max-height: 200px; diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 54a452f8c3d7e..82c4d8be2c660 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -7,7 +7,7 @@ import './media/chatWidget.css'; import './media/chatWelcomePart.css'; import * as dom from '../../../../base/browser/dom.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { toAction } from '../../../../base/common/actions.js'; +import { Separator, toAction } from '../../../../base/common/actions.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; @@ -30,12 +30,12 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { isEqual } from '../../../../base/common/resources.js'; +import { basename, isEqual } from '../../../../base/common/resources.js'; import { asCSSUrl } from '../../../../base/browser/cssValue.js'; import { FileAccess } from '../../../../base/common/network.js'; import { localize } from '../../../../nls.js'; import { AgentSessionProviders, getAgentSessionProviderIcon } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js'; -import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js'; +import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { ChatSessionPosition, getResourceForNewChatSession } from '../../../../workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.js'; import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from '../../../../workbench/contrib/chat/browser/chatSessions/chatSessionPickerActionItem.js'; import { SearchableOptionPickerActionItem } from '../../../../workbench/contrib/chat/browser/chatSessions/searchableOptionPickerActionItem.js'; @@ -47,6 +47,10 @@ import { IModelPickerDelegate, ModelPickerActionItem } from '../../../../workben import { IChatInputPickerOptions } from '../../../../workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.js'; import { WorkspaceFolderCountContext } from '../../../../workbench/common/contextkeys.js'; import { IViewDescriptorService } from '../../../../workbench/common/views.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IWorkspaceEditingService } from '../../../../workbench/services/workspaces/common/workspaceEditing.js'; +import { IWorkspacesService, isRecentFolder } from '../../../../platform/workspaces/common/workspaces.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'; @@ -104,6 +108,20 @@ class TargetConfig extends Disposable implements ITargetConfig { this._onDidChangeSelectedTarget.fire(target); } } + + setAllowedTargets(targets: AgentSessionProviders[]): void { + const newSet = new Set(targets); + this._allowedTargets.set(newSet, undefined); + this._onDidChangeAllowedTargets.fire(newSet); + + // If the currently selected target is no longer allowed, switch to the first allowed target + const current = this._selectedTarget.get(); + if (current && !newSet.has(current)) { + const fallback = newSet.values().next().value; + this._selectedTarget.set(fallback, undefined); + this._onDidChangeSelectedTarget.fire(fallback); + } + } } // #endregion @@ -139,7 +157,7 @@ export interface INewChatWidgetOptions { */ class NewChatWidget extends Disposable { - private readonly _targetConfig: ITargetConfig; + private readonly _targetConfig: TargetConfig; private readonly _options: INewChatWidgetOptions; // Input @@ -174,6 +192,10 @@ class NewChatWidget extends Disposable { @IProductService private readonly productService: IProductService, @ILogService private readonly logService: ILogService, @IHoverService _hoverService: IHoverService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, + @IWorkspacesService private readonly workspacesService: IWorkspacesService, ) { super(); this._targetConfig = this._register(new TargetConfig(options.targetConfig)); @@ -397,16 +419,16 @@ class NewChatWidget extends Disposable { const pickersRow = dom.append(this._pickersContainer, dom.$('.chat-full-welcome-pickers')); - // Left group: target dropdown + non-repo extension pickers + // Left group: target dropdown (agent/worktree picker) const leftGroup = dom.append(pickersRow, dom.$('.sessions-chat-pickers-left')); this._targetDropdownContainer = dom.append(leftGroup, dom.$('.sessions-chat-dropdown-wrapper')); this._renderTargetDropdown(this._targetDropdownContainer); - this._extensionPickersLeftContainer = dom.append(leftGroup, dom.$('.sessions-chat-extension-pickers-left')); // Spacer dom.append(pickersRow, dom.$('.sessions-chat-pickers-spacer')); - // Right group: repo/folder pickers + // Right group: all extension pickers (folder first, then others) + this._extensionPickersLeftContainer = undefined; this._extensionPickersRightContainer = dom.append(pickersRow, dom.$('.sessions-chat-extension-pickers-right')); this._renderExtensionPickers(); @@ -434,7 +456,6 @@ class NewChatWidget extends Disposable { const currentAllowed = this._targetConfig.allowedTargets.get(); const currentActive = this._targetConfig.selectedTarget.get(); const actions = [...currentAllowed] - .filter(t => t !== AgentSessionProviders.Local) .map(sessionType => { const label = getAgentSessionProviderName(sessionType); return toAction({ @@ -462,7 +483,7 @@ class NewChatWidget extends Disposable { // --- Welcome: Extension option pickers --- private _renderExtensionPickers(force?: boolean): void { - if (!this._extensionPickersLeftContainer || !this._extensionPickersRightContainer) { + if (!this._extensionPickersRightContainer) { return; } @@ -472,8 +493,16 @@ class NewChatWidget extends Disposable { return; } + // For Local target, render a workspace folder picker instead of extension pickers + if (activeSessionType === AgentSessionProviders.Local) { + this._clearExtensionPickers(); + this._renderLocalFolderPicker(); + return; + } + const optionGroups = this.chatSessionsService.getOptionGroupsForSessionType(activeSessionType); if (!optionGroups || optionGroups.length === 0) { + this._clearExtensionPickers(); return; } @@ -503,7 +532,15 @@ class NewChatWidget extends Disposable { return; } - visibleGroups.sort((a, b) => (a.when ? 1 : 0) - (b.when ? 1 : 0)); + visibleGroups.sort((a, b) => { + // Repo/folder pickers first, then others + const aRepo = isRepoOrFolderGroup(a) ? 0 : 1; + const bRepo = isRepoOrFolderGroup(b) ? 0 : 1; + if (aRepo !== bRepo) { + return aRepo - bRepo; + } + return (a.when ? 1 : 0) - (b.when ? 1 : 0); + }); if (!force && this._pickerWidgets.size === visibleGroups.length) { const allMatch = visibleGroups.every(g => this._pickerWidgets.has(g.id)); @@ -556,15 +593,75 @@ class NewChatWidget extends Disposable { this._pickerWidgetDisposables.add(widget); this._pickerWidgets.set(optionGroup.id, widget); - // Repo/folder pickers go to the right; others go to the left - const isRightAligned = isRepoOrFolderGroup(optionGroup); - const targetContainer = isRightAligned ? this._extensionPickersRightContainer! : this._extensionPickersLeftContainer!; + // All pickers go to the right + const targetContainer = this._extensionPickersRightContainer!; const slot = dom.append(targetContainer, dom.$('.sessions-chat-picker-slot')); widget.render(slot); } } + private _renderLocalFolderPicker(): void { + if (!this._extensionPickersRightContainer) { + return; + } + + const folders = this.workspaceContextService.getWorkspace().folders; + const currentFolder = folders[0]; + const folderName = currentFolder ? basename(currentFolder.uri) : localize('noFolder', "No Folder"); + + const slot = dom.append(this._extensionPickersRightContainer, dom.$('.sessions-chat-picker-slot')); + const button = dom.append(slot, dom.$('.sessions-chat-dropdown-button')); + button.tabIndex = 0; + button.role = 'button'; + button.ariaHasPopup = 'true'; + dom.append(button, renderIcon(Codicon.folder)); + dom.append(button, dom.$('span.sessions-chat-dropdown-label', undefined, folderName)); + dom.append(button, renderIcon(Codicon.chevronDown)); + + const switchFolder = async (folderUri: URI) => { + const foldersToDelete = this.workspaceContextService.getWorkspace().folders.map(f => f.uri); + await this.workspaceEditingService.updateFolders(0, foldersToDelete.length, [{ uri: folderUri }]); + this._renderExtensionPickers(true); + }; + + this._pickerWidgetDisposables.add(dom.addDisposableListener(button, dom.EventType.CLICK, async () => { + const recentlyOpened = await this.workspacesService.getRecentlyOpened(); + const recentFolders = recentlyOpened.workspaces + .filter(isRecentFolder) + .filter(r => !currentFolder || !isEqual(r.folderUri, currentFolder.uri)) + .slice(0, 10); + + const actions = recentFolders.map(recent => toAction({ + id: recent.folderUri.toString(), + label: recent.label || basename(recent.folderUri), + run: () => switchFolder(recent.folderUri), + })); + + actions.push(new Separator()); + actions.push(toAction({ + id: 'browse', + label: localize('browseFolder', "Browse..."), + run: async () => { + const selected = await this.fileDialogService.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + title: localize('selectFolder', "Select Folder"), + }); + if (selected?.[0]) { + await switchFolder(selected[0]); + } + }, + })); + + this.contextMenuService.showContextMenu({ + getAnchor: () => button, + getActions: () => actions, + }); + })); + } + private _evaluateOptionGroupVisibility(optionGroup: { id: string; when?: string }): boolean { if (!optionGroup.when) { return true; @@ -704,6 +801,10 @@ class NewChatWidget extends Disposable { focusInput(): void { this._editor?.focus(); } + + updateAllowedTargets(targets: AgentSessionProviders[]): void { + this._targetConfig.setAllowedTargets(targets); + } } // #endregion @@ -730,7 +831,8 @@ export class NewChatViewPane extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @IHoverService hoverService: IHoverService, - @ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService, + @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @ILogService private readonly logService: ILogService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -743,7 +845,7 @@ export class NewChatViewPane extends ViewPane { NewChatWidget, { targetConfig: { - allowedTargets: [AgentSessionProviders.Background, AgentSessionProviders.Cloud], + allowedTargets: this.computeAllowedTargets(), defaultTarget: AgentSessionProviders.Background, }, onSendRequest: (data) => { @@ -756,6 +858,19 @@ export class NewChatViewPane extends ViewPane { this._widget.render(container); this._widget.focusInput(); + + this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => { + this._widget?.updateAllowedTargets(this.computeAllowedTargets()); + })); + } + + private computeAllowedTargets(): AgentSessionProviders[] { + const targets: AgentSessionProviders[] = []; + if (this.workspaceContextService.getWorkspace().folders.length === 1) { + targets.push(AgentSessionProviders.Local); + } + targets.push(AgentSessionProviders.Background, AgentSessionProviders.Cloud); + return targets; } protected override layoutBody(height: number, width: number): void { diff --git a/src/vs/sessions/contrib/chat/browser/promptsService.ts b/src/vs/sessions/contrib/chat/browser/promptsService.ts index 754a3f995ee2e..69c0e8e2497dd 100644 --- a/src/vs/sessions/contrib/chat/browser/promptsService.ts +++ b/src/vs/sessions/contrib/chat/browser/promptsService.ts @@ -17,7 +17,7 @@ import { IWorkbenchEnvironmentService } from '../../../../workbench/services/env import { IPathService } from '../../../../workbench/services/path/common/pathService.js'; import { ISearchService } from '../../../../workbench/services/search/common/search.js'; import { IUserDataProfileService } from '../../../../workbench/services/userDataProfile/common/userDataProfile.js'; -import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js'; +import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; export class AgenticPromptsService extends PromptsService { protected override createPromptFilesLocator(): PromptFilesLocator { @@ -36,7 +36,7 @@ class AgenticPromptFilesLocator extends PromptFilesLocator { @IUserDataProfileService userDataService: IUserDataProfileService, @ILogService logService: ILogService, @IPathService pathService: IPathService, - @ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService, + @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, ) { super( fileService, diff --git a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts index e9368f846a56c..6138f48d98e69 100644 --- a/src/vs/sessions/contrib/chat/browser/runScriptAction.ts +++ b/src/vs/sessions/contrib/chat/browser/runScriptAction.ts @@ -13,7 +13,7 @@ import { IQuickInputService } from '../../../../platform/quickinput/common/quick import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; -import { ISessionsWorkbenchService } from '../../sessions/browser/sessionsWorkbenchService.js'; +import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; import { ITerminalService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; import { Menus } from '../../../browser/menus.js'; @@ -54,7 +54,7 @@ export class RunScriptContribution extends Disposable implements IWorkbenchContr constructor( @IStorageService private readonly _storageService: IStorageService, @ITerminalService private readonly _terminalService: ITerminalService, - @ISessionsWorkbenchService activeSessionService: ISessionsWorkbenchService, + @ISessionsManagementService activeSessionService: ISessionsManagementService, @IQuickInputService private readonly _quickInputService: IQuickInputService, ) { super(); diff --git a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts index 40dbca7917b74..db053fcef2266 100644 --- a/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts +++ b/src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts @@ -23,6 +23,7 @@ Registry.as(Extensions.Configuration).registerDefaultCon 'git.showProgress': false, 'github.copilot.chat.claudeCode.enabled': true, + 'github.copilot.chat.cli.branchSupport.enabled': true, 'github.copilot.chat.languageContext.typescript.enabled': true, 'inlineChat.affordance': 'editor', diff --git a/src/vs/sessions/contrib/sessions/browser/sessions.contribution.ts b/src/vs/sessions/contrib/sessions/browser/sessions.contribution.ts index c5211ee8e27a6..52bd441711531 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessions.contribution.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessions.contribution.ts @@ -14,7 +14,7 @@ import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../work import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { SessionsTitleBarContribution } from './sessionsTitleBarWidget.js'; import { AgenticSessionsViewPane, SessionsViewId } from './sessionsViewPane.js'; -import { SessionsWorkbenchService, ISessionsWorkbenchService } from './sessionsWorkbenchService.js'; +import { SessionsManagementService, ISessionsManagementService } from './sessionsManagementService.js'; const agentSessionsViewIcon = registerIcon('chat-sessions-icon', Codicon.commentDiscussionSparkle, localize('agentSessionsViewIcon', 'Icon for Agent Sessions View')); const AGENT_SESSIONS_VIEW_TITLE = localize2('agentSessions.view.label', "Sessions"); @@ -47,4 +47,4 @@ Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews registerWorkbenchContribution2(SessionsTitleBarContribution.ID, SessionsTitleBarContribution, WorkbenchPhase.AfterRestored); -registerSingleton(ISessionsWorkbenchService, SessionsWorkbenchService, InstantiationType.Delayed); +registerSingleton(ISessionsManagementService, SessionsManagementService, InstantiationType.Delayed); diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsWorkbenchService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts similarity index 85% rename from src/vs/sessions/contrib/sessions/browser/sessionsWorkbenchService.ts rename to src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index 84368fd05f9a7..8686c11d381b2 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsWorkbenchService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IObservable, observableValue } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; @@ -16,7 +16,7 @@ import { ChatViewPaneTarget, IChatWidgetService } from '../../../../workbench/co import { IChatSessionItem, IChatSessionProviderOptionItem, IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { IChatService, IChatSendRequestOptions } from '../../../../workbench/contrib/chat/common/chatService/chatService.js'; import { ChatAgentLocation } from '../../../../workbench/contrib/chat/common/constants.js'; -import { IAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; +import { IAgentSession, isAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; export const IsNewChatSessionContext = new RawContextKey('isNewChatSession', true); @@ -43,7 +43,7 @@ export type IActiveSessionItem = (IChatSessionItem | IAgentSession) & { readonly worktree: URI | undefined; }; -export interface ISessionsWorkbenchService { +export interface ISessionsManagementService { readonly _serviceBrand: undefined; /** @@ -75,9 +75,9 @@ export interface ISessionsWorkbenchService { openNewSession(): void; } -export const ISessionsWorkbenchService = createDecorator('sessionsWorkbenchService'); +export const ISessionsManagementService = createDecorator('sessionsManagementService'); -export class SessionsWorkbenchService extends Disposable implements ISessionsWorkbenchService { +export class SessionsManagementService extends Disposable implements ISessionsManagementService { declare readonly _serviceBrand: undefined; @@ -137,6 +137,12 @@ export class SessionsWorkbenchService extends Disposable implements ISessionsWor const agentSession = this.agentSessionsService.model.getSession(currentActive.resource); if (!agentSession) { + // Only switch sessions if the active session was a known agent session + // that got deleted. New session resources that aren't yet in the model + // should not trigger a switch. + if (isAgentSession(currentActive)) { + this.showNextSession(); + } return; } @@ -149,6 +155,19 @@ export class SessionsWorkbenchService extends Disposable implements ISessionsWor this._activeSession.set(activeSessionItem, undefined); } + private showNextSession(): void { + const sessions = this.agentSessionsService.model.sessions + .filter(s => !s.isArchived()) + .sort((a, b) => (b.timing.lastRequestEnded ?? b.timing.created) - (a.timing.lastRequestEnded ?? a.timing.created)); + + if (sessions.length > 0) { + this.setActiveSession(sessions[0]); + this.instantiationService.invokeFunction(openSessionDefault, sessions[0]); + } else { + this.openNewSession(); + } + } + private getRepositoryFromMetadata(metadata: { readonly [key: string]: unknown } | undefined): [URI | undefined, URI | undefined] { if (!metadata) { return [undefined, undefined]; @@ -189,15 +208,18 @@ export class SessionsWorkbenchService extends Disposable implements ISessionsWor } async openSession(sessionResource: URI, openOptions?: ISessionOpenOptions): Promise { - this.isNewChatSessionContext.set(false); const session = this.agentSessionsService.model.getSession(sessionResource); if (session) { + this.isNewChatSessionContext.set(false); this.setActiveSession(session); await this.instantiationService.invokeFunction(openSessionDefault, session, openOptions); } else { // For new sessions, load via the chat service first so the model // is ready before the ChatViewPane renders it. const modelRef = await this.chatService.loadSessionForResource(sessionResource, ChatAgentLocation.Chat, CancellationToken.None); + // Switch view only after the model is loaded so the ChatViewPane + // has content immediately when it becomes visible. + this.isNewChatSessionContext.set(false); const chatWidget = await this.chatWidgetService.openSession(sessionResource, ChatViewPaneTarget); if (!chatWidget?.viewModel) { this.logService.warn(`[ActiveSessionService] Failed to open session: ${sessionResource.toString()}`); @@ -255,12 +277,31 @@ export class SessionsWorkbenchService extends Disposable implements ISessionsWor return; } - // 5. After send, the extension creates an agent session. Detect it + // 5. After send, the extension creates an agent session. Wait for it // and set it as the active session so the titlebar and sidebar // reflect the new session. - const newSession = this.agentSessionsService.model.sessions.find( + let newSession = this.agentSessionsService.model.sessions.find( s => !existingResources.has(s.resource.toString()) ); + + if (!newSession) { + let listener: IDisposable | undefined; + newSession = await Promise.race([ + new Promise(resolve => { + listener = this.agentSessionsService.model.onDidChangeSessions(() => { + const session = this.agentSessionsService.model.sessions.find( + s => !existingResources.has(s.resource.toString()) + ); + if (session) { + resolve(session); + } + }); + }), + new Promise(resolve => setTimeout(() => resolve(undefined), 30_000)), + ]); + listener?.dispose(); + } + if (newSession) { this.setActiveSession(newSession); } diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts index 5d271a457ed33..faba73697edfb 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsTitleBarWidget.ts @@ -18,7 +18,7 @@ import { IWorkbenchContribution } from '../../../../workbench/common/contributio import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; import { URI } from '../../../../base/common/uri.js'; import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js'; -import { ISessionsWorkbenchService } from './sessionsWorkbenchService.js'; +import { ISessionsManagementService } from './sessionsManagementService.js'; import { FocusAgentSessionsAction } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsActions.js'; import { AgentSessionsPicker } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsPicker.js'; import { autorun } from '../../../../base/common/observable.js'; @@ -60,7 +60,7 @@ export class SessionsTitleBarWidget extends BaseActionViewItem { options: IBaseActionViewItemOptions | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, @IHoverService private readonly hoverService: IHoverService, - @ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService, + @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, @IChatService private readonly chatService: IChatService, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @ICommandService private readonly commandService: ICommandService, diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts index 63d62b4fa9d3a..8f6af610ad5c2 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts @@ -23,7 +23,7 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { localize, localize2 } from '../../../../nls.js'; import { AgentSessionsControl } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsControl.js'; import { AgentSessionsFilter, AgentSessionsGrouping } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.js'; -import { ISessionsWorkbenchService } from './sessionsWorkbenchService.js'; +import { ISessionsManagementService } from './sessionsManagementService.js'; import { Action2, ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js'; @@ -98,7 +98,7 @@ export class AgenticSessionsViewPane extends ViewPane { @IMcpService private readonly mcpService: IMcpService, @IStorageService private readonly storageService: IStorageService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @ISessionsWorkbenchService private readonly activeSessionService: ISessionsWorkbenchService, + @ISessionsManagementService private readonly activeSessionService: ISessionsManagementService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index bebcf70f3b357..6870246190f5b 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -864,7 +864,7 @@ export class AccessibleView extends Disposable { } private _navigationHint(): string { - return localize('accessibleViewNextPreviousHint', "Show the next item{0} or previous item{1}.", ``); + return localize('accessibleViewNextPreviousHint', "Show the next item{0} or previous item{1}.", ``, ``); } private _disableVerbosityHint(provider: AccesibleViewContentProvider): string { diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts index 43ce32c896b38..d1bd6f3d4c8fb 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts @@ -55,7 +55,12 @@ class OpenIntegratedBrowserAction extends Action2 { logBrowserOpen(telemetryService, options.url ? 'commandWithUrl' : 'commandWithoutUrl'); - await editorService.openEditor({ resource }, group); + const editorPane = await editorService.openEditor({ resource }, group); + + // Lock the group when opening to the side + if (options.openToSide && editorPane?.group) { + editorPane.group.lock(true); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts index f1ec099751c11..7be04e1760c8d 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts @@ -9,6 +9,7 @@ import { Action2, registerAction2 } from '../../../../../platform/actions/common import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IChatWidgetService } from '../chat.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { isResponseVM } from '../../common/model/chatViewModel.js'; @@ -27,7 +28,7 @@ class AnnounceChatConfirmationAction extends Action2 { keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KeyA | KeyMod.Shift, - when: CONTEXT_ACCESSIBILITY_MODE_ENABLED + when: ContextKeyExpr.and(CONTEXT_ACCESSIBILITY_MODE_ENABLED, ChatContextKeys.Editing.hasQuestionCarousel.negate()) } }); } @@ -60,7 +61,12 @@ class AnnounceChatConfirmationAction extends Action2 { } if (firstConfirmationElement) { - firstConfirmationElement.focus(); + // Toggle: if the confirmation is already focused, move focus back to input + if (firstConfirmationElement.contains(pendingWidget.domNode.ownerDocument.activeElement)) { + pendingWidget.focusInput(); + } else { + firstConfirmationElement.focus(); + } } else { alert(localize('noConfirmationRequired', 'No chat confirmation required')); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index e020ad9808cca..7ac9212392892 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -60,11 +60,14 @@ import { ILanguageModelChatSelector, ILanguageModelsService } from '../../common import { CopilotUsageExtensionFeatureId } from '../../common/languageModelStats.js'; import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js'; import { ILanguageModelToolsService, IToolData, IToolSet, isToolSet } from '../../common/tools/languageModelToolsService.js'; -import { ChatViewId, IChatWidget, IChatWidgetService } from '../chat.js'; +import { ChatViewId, IChatWidget, IChatWidgetService, isIChatViewViewContext } from '../chat.js'; import { IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js'; import { ChatEditorInput, showClearEditingSessionConfirmation } from '../widgetHosts/editor/chatEditorInput.js'; import { convertBufferToScreenshotVariable } from '../attachments/chatScreenshotContext.js'; -import { LocalChatSessionUri } from '../../common/model/chatUri.js'; +import { getChatSessionType, LocalChatSessionUri } from '../../common/model/chatUri.js'; +import { localChatSessionType } from '../../common/chatSessionsService.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; +import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; export const CHAT_CATEGORY = localize2('chat.category', 'Chat'); @@ -899,8 +902,8 @@ export function registerChatActions() { precondition: ChatContextKeys.inChatSession, keybinding: [{ weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyH, - when: ChatContextKeys.inChatInput, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyA, + when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.Editing.hasQuestionCarousel), }] }); } @@ -1524,6 +1527,25 @@ export interface IClearEditingSessionConfirmationOptions { isArchiveAction?: boolean; } +/** + * Clears the current chat session and starts a new one, preserving + * the session type (e.g. Claude, Cloud, Background) for non-local sessions + * in the sidebar. + */ +export async function clearChatSessionPreservingType(widget: IChatWidget, viewsService: IViewsService, sessionType?: string): Promise { + const currentResource = widget.viewModel?.model.sessionResource; + const newSessionType = sessionType ?? (currentResource ? getChatSessionType(currentResource) : localChatSessionType); + if (isIChatViewViewContext(widget.viewContext) && newSessionType !== localChatSessionType) { + // For the sidebar, we need to explicitly load a session with the same type + const newResource = URI.from({ scheme: newSessionType, path: `/untitled-${generateUuid()}` }); + const view = await viewsService.openView(ChatViewId) as ChatViewPane; + await view.loadSession(newResource); + } else { + // For the editor, widget.clear() already preserves the session type via clearChatEditor + await widget.clear(); + } +} + // --- Chat Submenus in various Components diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 5291b8e36cbb1..bf6d4807280b1 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -21,6 +21,7 @@ import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.j import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; +import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { IChatMode, IChatModeService } from '../../common/chatModes.js'; import { chatVariableLeader } from '../../common/requestParser/chatParserTypes.js'; @@ -35,7 +36,7 @@ import { IChatWidget, IChatWidgetService } from '../chat.js'; import { getAgentSessionProvider, AgentSessionProviders } from '../agentSessions/agentSessions.js'; import { getEditingSessionContext } from '../chatEditing/chatEditingActions.js'; import { ctxHasEditorModification, ctxHasRequestInProgress, ctxIsGlobalEditingSession } from '../chatEditing/chatEditingEditorContextKeys.js'; -import { ACTION_ID_NEW_CHAT, CHAT_CATEGORY, handleCurrentEditingSession, handleModeSwitch } from './chatActions.js'; +import { ACTION_ID_NEW_CHAT, CHAT_CATEGORY, clearChatSessionPreservingType, handleCurrentEditingSession, handleModeSwitch } from './chatActions.js'; import { CreateRemoteAgentJobAction } from './chatContinueInAction.js'; export interface IVoiceChatExecuteActionContext { @@ -198,11 +199,11 @@ export class ChatSubmitAction extends SubmitAction { title: localize2('interactive.submit.label', "Send"), f1: false, category: CHAT_CATEGORY, - icon: Codicon.send, + icon: Codicon.arrowUp, precondition, toggled: { condition: ChatContextKeys.lockedToCodingAgent, - icon: Codicon.send, + icon: Codicon.arrowUp, tooltip: localize('sendToAgent', "Send to Agent"), }, keybinding: { @@ -676,7 +677,7 @@ export class ChatEditingSessionSubmitAction extends SubmitAction { title: localize2('edits.submit.label', "Send"), f1: false, category: CHAT_CATEGORY, - icon: Codicon.send, + icon: Codicon.arrowUp, precondition, menu: [ { @@ -799,6 +800,7 @@ class SendToNewChatAction extends Action2 { const context = args[0] as IChatExecuteActionContext | undefined; const widgetService = accessor.get(IChatWidgetService); + const viewsService = accessor.get(IViewsService); const dialogService = accessor.get(IDialogService); const chatService = accessor.get(IChatService); const widget = context?.widget ?? widgetService.lastFocusedWidget; @@ -822,7 +824,8 @@ class SendToNewChatAction extends Action2 { // Clear the input from the current session before creating a new one widget.setInput(''); - await widget.clear(); + await clearChatSessionPreservingType(widget, viewsService); + widget.acceptInput(inputBeforeClear, { storeToHistory: true }); } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts index 101377d162559..febdbc539f7fb 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts @@ -5,8 +5,6 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { generateUuid } from '../../../../../base/common/uuid.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { localize, localize2 } from '../../../../../nls.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; @@ -19,13 +17,10 @@ import { IViewsService } from '../../../../services/views/common/viewsService.js import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { IChatEditingSession } from '../../common/editing/chatEditingService.js'; import { IChatService } from '../../common/chatService/chatService.js'; -import { localChatSessionType } from '../../common/chatSessionsService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; -import { getChatSessionType, LocalChatSessionUri } from '../../common/model/chatUri.js'; -import { ChatViewId, IChatWidgetService, isIChatViewViewContext } from '../chat.js'; +import { ChatViewId, IChatWidgetService } from '../chat.js'; import { EditingSessionAction, EditingSessionActionContext, getEditingSessionContext } from '../chatEditing/chatEditingActions.js'; -import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; -import { ACTION_ID_NEW_CHAT, ACTION_ID_NEW_EDIT_SESSION, CHAT_CATEGORY, handleCurrentEditingSession } from './chatActions.js'; +import { ACTION_ID_NEW_CHAT, ACTION_ID_NEW_EDIT_SESSION, CHAT_CATEGORY, clearChatSessionPreservingType, handleCurrentEditingSession } from './chatActions.js'; import { clearChatEditor } from './chatClear.js'; import { AgentSessionProviders, AgentSessionsViewerOrientation } from '../agentSessions/agentSessions.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; @@ -262,23 +257,6 @@ export function registerNewChatActions() { }); } -/** - * Creates a new session resource URI with the specified session type. - * For remote sessions, creates a URI with the session type as the scheme. - * For local sessions, creates a LocalChatSessionUri. - */ -function getResourceForNewChatSession(sessionType: string): URI { - const isRemoteSession = sessionType !== localChatSessionType; - if (isRemoteSession) { - return URI.from({ - scheme: sessionType, - path: `/untitled-${generateUuid()}`, - }); - } - - return LocalChatSessionUri.forSession(generateUuid()); -} - async function runNewChatAction( accessor: ServicesAccessor, context: EditingSessionActionContext | undefined, @@ -303,18 +281,8 @@ async function runNewChatAction( await editingSession?.stop(); - // Create a new session with the same type as the current session - const currentResource = widget.viewModel?.model.sessionResource; - const newSessionType = sessionType ?? (currentResource ? getChatSessionType(currentResource) : localChatSessionType); - if (isIChatViewViewContext(widget.viewContext) && newSessionType !== localChatSessionType) { - // For the sidebar, we need to explicitly load a session with the same type - const newResource = getResourceForNewChatSession(newSessionType); - const view = await viewsService.openView(ChatViewId) as ChatViewPane; - await view.loadSession(newResource); - } else { - // For the editor, widget.clear() already preserves the session type via clearChatEditor - await widget.clear(); - } + // Create a new session, preserving the session type (or using the specified one) + await clearChatSessionPreservingType(widget, viewsService, sessionType); widget.attachmentModel.clear(true); widget.focusInput(); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 2a89832b7f4ee..bcc94cad564c3 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -219,7 +219,8 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre } // Separator (dot between badge and description) - template.separator.classList.toggle('has-separator', hasBadge && !hasDiff); + const hasDescription = template.description.textContent !== ''; + template.separator.classList.toggle('has-separator', hasBadge && hasDescription); // Status this.renderStatus(session, template); @@ -502,7 +503,7 @@ export class AgentSessionSectionRenderer implements ICompressibleTreeRenderer { - static readonly ITEM_HEIGHT = 44; + static readonly ITEM_HEIGHT = 48; static readonly SECTION_HEIGHT = 26; getHeight(element: AgentSessionListItem): number { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css index 5db250de5b30e..b9863aae7dcbc 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css @@ -82,7 +82,7 @@ .agent-session-item { display: flex; flex-direction: row; - padding: 4px 6px; + padding: 6px 6px; &.archived { color: var(--vscode-descriptionForeground); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index b30f87e3391a0..84d403c12ddfe 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -284,6 +284,7 @@ configurationRegistry.registerConfiguration({ }, 'chat.tips.enabled': { type: 'boolean', + scope: ConfigurationScope.APPLICATION, description: nls.localize('chat.tips.enabled', "Controls whether tips are shown above user messages in chat. This is an experimental feature."), default: false, tags: ['experimental'], @@ -688,15 +689,10 @@ configurationRegistry.registerConfiguration({ default: true, tags: ['experimental'], }, - ['chat.statusWidget.sku']: { - type: 'string', - enum: ['free', 'anonymous'], - enumDescriptions: [ - nls.localize('chat.statusWidget.sku.free', "Show status widget for free tier users."), - nls.localize('chat.statusWidget.sku.anonymous', "Show status widget for anonymous users.") - ], - description: nls.localize('chat.statusWidget.enabled.description', "Controls which user type should see the status widget in new chat sessions when quota is exceeded."), - default: undefined, + ['chat.statusWidget.anonymous']: { + type: 'boolean', + description: nls.localize('chat.statusWidget.anonymous.description', "Controls whether anonymous users see the status widget in new chat sessions when rate limited."), + default: false, tags: ['experimental', 'advanced'], experiment: { mode: 'auto' @@ -1091,6 +1087,24 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('chat.agent.thinking.terminalTools', "When enabled, terminal tool calls are displayed inside the thinking dropdown with a simplified view."), tags: ['experimental'], }, + 'chat.tools.usagesTool.enabled': { + type: 'boolean', + default: true, + markdownDescription: nls.localize('chat.tools.usagesTool.enabled', "Controls whether the usages tool is available for finding references, definitions, and implementations of code symbols."), + tags: ['preview'], + experiment: { + mode: 'auto' + } + }, + 'chat.tools.renameTool.enabled': { + type: 'boolean', + default: true, + markdownDescription: nls.localize('chat.tools.renameTool.enabled', "Controls whether the rename tool is available for renaming code symbols across the workspace."), + tags: ['preview'], + experiment: { + mode: 'auto' + } + }, [ChatConfiguration.AutoExpandToolFailures]: { type: 'boolean', default: true, 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 cb8b452684a93..bb70941b5ac4d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -993,15 +993,25 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } public async getOrCreateChatSession(sessionResource: URI, token: CancellationToken): Promise { - const existingSessionData = this._sessions.get(sessionResource); - if (existingSessionData) { - return existingSessionData.session; + { + const existingSessionData = this._sessions.get(sessionResource); + if (existingSessionData) { + return existingSessionData.session; + } } if (!(await raceCancellationError(this.canResolveChatSession(sessionResource), token))) { throw Error(`Can not find provider for ${sessionResource}`); } + // Check again after async provider resolution + { + const existingSessionData = this._sessions.get(sessionResource); + if (existingSessionData) { + return existingSessionData.session; + } + } + const resolvedType = this._resolveToPrimaryType(sessionResource.scheme) || sessionResource.scheme; const provider = this._contentProviders.get(resolvedType); if (!provider) { @@ -1009,6 +1019,16 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } const session = await raceCancellationError(provider.provideChatSessionContent(sessionResource, token), token); + + // Make sure another session wasn't created while we were awaiting the provider + { + const existingSessionData = this._sessions.get(sessionResource); + if (existingSessionData) { + session.dispose(); + return existingSessionData.session; + } + } + const sessionData = new ContributedChatSessionData(session, sessionResource.scheme, sessionResource, session.options, resource => { sessionData.dispose(); this._sessions.delete(resource); @@ -1016,6 +1036,9 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ this._sessions.set(sessionResource, sessionData); + // Make sure any listeners are aware of the new session and its options + this._onDidChangeSessionOptions.fire(sessionResource); + return session; } diff --git a/src/vs/workbench/contrib/chat/browser/chatTipService.ts b/src/vs/workbench/contrib/chat/browser/chatTipService.ts index 38b5d9fa91b03..3a5d17812a83c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatTipService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatTipService.ts @@ -10,7 +10,7 @@ import { createDecorator, IInstantiationService } from '../../../../platform/ins import { IProductService } from '../../../../platform/product/common/productService.js'; import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; import { ChatAgentLocation, ChatModeKind } from '../common/constants.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; @@ -112,6 +112,11 @@ export interface ITipDefinition { * Command IDs that are allowed to be executed from this tip's markdown. */ readonly enabledCommands?: string[]; + /** + * Chat model IDs for which this tip is eligible. + * Compared against the lowercased `chatModelId` context key. + */ + readonly onlyWhenModelIds?: readonly string[]; /** * Command IDs that, if ever executed in this workspace, make this tip ineligible. * The tip won't be shown if the user has already performed the action it suggests. @@ -144,6 +149,12 @@ export interface ITipDefinition { * Static catalog of tips. Each tip has an optional when clause for eligibility. */ const TIP_CATALOG: ITipDefinition[] = [ + { + id: 'tip.switchToAuto', + message: localize('tip.switchToAuto', "Tip: Using gpt-4.1? Try switching to [Auto](command:workbench.action.chat.openModelPicker) in the model picker for better coding performance."), + enabledCommands: ['workbench.action.chat.openModelPicker'], + onlyWhenModelIds: ['gpt-4.1'], + }, { id: 'tip.agentMode', message: localize('tip.agentMode', "Tip: Try [Agents](command:workbench.action.chat.openEditSession) to make edits across your project and run commands."), @@ -588,7 +599,7 @@ export class ChatTipService extends Disposable implements IChatTipService { async disableTips(): Promise { this._shownTip = undefined; this._tipRequestId = undefined; - await this._configurationService.updateValue('chat.tips.enabled', false); + await this._configurationService.updateValue('chat.tips.enabled', false, ConfigurationTarget.APPLICATION); this._onDidDisableTips.fire(); } @@ -615,6 +626,16 @@ export class ChatTipService extends Disposable implements IChatTipService { // Return the already-shown tip for stable rerenders if (this._tipRequestId === 'welcome' && this._shownTip) { + if (!this._isEligible(this._shownTip, contextKeyService)) { + const nextTip = this._findNextEligibleTip(this._shownTip.id, contextKeyService); + if (nextTip) { + this._shownTip = nextTip; + this._storageService.store(ChatTipService._LAST_TIP_ID_KEY, nextTip.id, StorageScope.PROFILE, StorageTarget.USER); + const tip = this._createTip(nextTip); + this._onDidNavigateTip.fire(tip); + return tip; + } + } return this._createTip(this._shownTip); } @@ -623,6 +644,24 @@ export class ChatTipService extends Disposable implements IChatTipService { return tip; } + private _findNextEligibleTip(currentTipId: string, contextKeyService: IContextKeyService): ITipDefinition | undefined { + const currentIndex = TIP_CATALOG.findIndex(tip => tip.id === currentTipId); + if (currentIndex === -1) { + return undefined; + } + + const dismissedIds = new Set(this._getDismissedTipIds()); + for (let i = 1; i < TIP_CATALOG.length; i++) { + const idx = (currentIndex + i) % TIP_CATALOG.length; + const candidate = TIP_CATALOG[idx]; + if (!dismissedIds.has(candidate.id) && this._isEligible(candidate, contextKeyService)) { + return candidate; + } + } + + return undefined; + } + private _pickTip(sourceId: string, contextKeyService: IContextKeyService): IChatTip | undefined { // Record the current mode for future eligibility decisions. this._tracker.recordCurrentMode(contextKeyService); @@ -711,6 +750,13 @@ export class ChatTipService extends Disposable implements IChatTipService { } private _isEligible(tip: ITipDefinition, contextKeyService: IContextKeyService): boolean { + if (tip.onlyWhenModelIds?.length) { + const currentModelId = this._getCurrentChatModelId(contextKeyService); + const isModelMatch = tip.onlyWhenModelIds.some(modelId => currentModelId === modelId || currentModelId.startsWith(`${modelId}-`)); + if (!isModelMatch) { + return false; + } + } if (tip.when && !contextKeyService.contextMatchesRules(tip.when)) { this._logService.debug('#ChatTips: tip is not eligible due to when clause', tip.id, tip.when.serialize()); return false; @@ -722,6 +768,42 @@ export class ChatTipService extends Disposable implements IChatTipService { return true; } + private _getCurrentChatModelId(contextKeyService: IContextKeyService): string { + const normalize = (modelId: string | undefined): string => { + const normalizedModelId = modelId?.toLowerCase() ?? ''; + if (!normalizedModelId) { + return ''; + } + + if (normalizedModelId.includes('/')) { + return normalizedModelId.split('/').at(-1) ?? ''; + } + + return normalizedModelId; + }; + + const contextKeyModelId = normalize(contextKeyService.getContextKeyValue(ChatContextKeys.chatModelId.key)); + if (contextKeyModelId) { + return contextKeyModelId; + } + + const location = contextKeyService.getContextKeyValue(ChatContextKeys.location.key) ?? ChatAgentLocation.Chat; + const sessionType = contextKeyService.getContextKeyValue(ChatContextKeys.chatSessionType.key) ?? ''; + const candidateStorageKeys = sessionType + ? [`chat.currentLanguageModel.${location}.${sessionType}`, `chat.currentLanguageModel.${location}`] + : [`chat.currentLanguageModel.${location}`]; + + for (const storageKey of candidateStorageKeys) { + const persistedModelIdentifier = this._storageService.get(storageKey, StorageScope.APPLICATION); + const persistedModelId = normalize(persistedModelIdentifier); + if (persistedModelId) { + return persistedModelId; + } + } + + return ''; + } + private _isChatLocation(contextKeyService: IContextKeyService): boolean { const location = contextKeyService.getContextKeyValue(ChatContextKeys.location.key); return !location || location === ChatAgentLocation.Chat; diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts index da74b4da74dac..5113db79b5cfe 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/pickers/promptFilePickers.ts @@ -318,8 +318,8 @@ const MAKE_VISIBLE_BUTTON: IQuickInputButton = { * Button that sets a prompt file to be invisible. */ const MAKE_INVISIBLE_BUTTON: IQuickInputButton = { - tooltip: localize('makeInvisible', "Hide from agent picker"), - iconClass: ThemeIcon.asClassName(Codicon.eyeClosed), + tooltip: localize('makeInvisible', "Shown in chat view agent picker. Click to hide."), + iconClass: ThemeIcon.asClassName(Codicon.eye), }; export class PromptFilePickers { diff --git a/src/vs/workbench/contrib/chat/browser/tools/renameTool.ts b/src/vs/workbench/contrib/chat/browser/tools/renameTool.ts index dcaa3e1e6d710..fef06150ff0a7 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/renameTool.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/renameTool.ts @@ -17,6 +17,7 @@ import { ILanguageFeaturesService } from '../../../../../editor/common/services/ import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { rename } from '../../../../../editor/contrib/rename/browser/rename.js'; import { localize } from '../../../../../nls.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; @@ -88,6 +89,7 @@ export class RenameTool extends Disposable implements IToolImpl { userDescription: localize('tool.rename.userDescription', 'Rename a symbol across the workspace'), modelDescription, source: ToolDataSource.Internal, + when: ContextKeyExpr.has('config.chat.tools.renameTool.enabled'), inputSchema: { type: 'object', properties: { diff --git a/src/vs/workbench/contrib/chat/browser/tools/usagesTool.ts b/src/vs/workbench/contrib/chat/browser/tools/usagesTool.ts index 8e9617b3c7ef6..075977bc799a1 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/usagesTool.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/usagesTool.ts @@ -20,6 +20,7 @@ import { ILanguageFeaturesService } from '../../../../../editor/common/services/ import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { getDefinitionsAtPosition, getImplementationsAtPosition, getReferencesAtPosition } from '../../../../../editor/contrib/gotoSymbol/browser/goToSymbol.js'; import { localize } from '../../../../../nls.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; @@ -85,6 +86,7 @@ export class UsagesTool extends Disposable implements IToolImpl { userDescription: localize('tool.usages.userDescription', 'Find references, definitions, and implementations of a symbol'), modelDescription, source: ToolDataSource.Internal, + when: ContextKeyExpr.has('config.chat.tools.usagesTool.enabled'), inputSchema: { type: 'object', properties: { 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 6a0b310b92555..d1284a8d1e601 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatQuestionCarouselPart.ts @@ -4,13 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../../base/browser/dom.js'; +import { renderAsPlaintext } from '../../../../../../base/browser/markdownRenderer.js'; import { StandardKeyboardEvent } from '../../../../../../base/browser/keyboardEvent.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../../../base/common/htmlContent.js'; import { KeyCode } from '../../../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { hasKey } from '../../../../../../base/common/types.js'; import { localize } from '../../../../../../nls.js'; import { IAccessibilityService } from '../../../../../../platform/accessibility/common/accessibility.js'; +import { IMarkdownRendererService } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; import { defaultButtonStyles, defaultCheckboxStyles, defaultInputBoxStyles } from '../../../../../../platform/theme/browser/defaultStyles.js'; import { Button } from '../../../../../../base/browser/ui/button/button.js'; import { InputBox } from '../../../../../../base/browser/ui/inputbox/inputBox.js'; @@ -55,6 +58,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent private readonly _multiSelectCheckboxes: Map = new Map(); private readonly _freeformTextareas: Map = new Map(); private readonly _inputBoxes: DisposableStore = this._register(new DisposableStore()); + private readonly _questionRenderStore = this._register(new MutableDisposable()); /** * Disposable store for interactive UI components (header, nav buttons, etc.) @@ -66,6 +70,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent public readonly carousel: IChatQuestionCarousel, context: IChatContentPartRenderContext, private readonly _options: IChatQuestionCarouselOptions, + @IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService, @IHoverService private readonly _hoverService: IHoverService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, ) { @@ -233,7 +238,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const question = this.carousel.questions[this._currentIndex]; if (question) { const questionText = question.message ?? question.title; - const messageContent = typeof questionText === 'string' ? questionText : questionText.value; + const messageContent = this.getQuestionText(questionText); const questionCount = this.carousel.questions.length; const alertMessage = questionCount === 1 ? messageContent @@ -265,6 +270,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent private clearInteractiveResources(): void { // Dispose interactive UI disposables (header, nav buttons, etc.) this._interactiveUIStore.clear(); + this._questionRenderStore.clear(); this._inputBoxes.clear(); this._textInputBoxes.clear(); this._singleSelectItems.clear(); @@ -400,7 +406,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent } const questionText = question.message ?? question.title; - const messageContent = typeof questionText === 'string' ? questionText : questionText.value; + const messageContent = this.getQuestionText(questionText); const questionCount = this.carousel.questions.length; if (questionCount === 1) { @@ -429,6 +435,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent return; } + const questionRenderStore = new DisposableStore(); + this._questionRenderStore.value = questionRenderStore; + // Clear previous input boxes and stale references this._inputBoxes.clear(); this._textInputBoxes.clear(); @@ -452,26 +461,29 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent const questionText = question.message ?? question.title; if (questionText) { const title = dom.$('.chat-question-title'); - const messageContent = typeof questionText === 'string' - ? questionText - : questionText.value; + const messageContent = this.getQuestionText(questionText); title.setAttribute('aria-label', messageContent); - // Check for subtitle in parentheses at the end - const parenMatch = messageContent.match(/^(.+?)\s*(\([^)]+\))\s*$/); - if (parenMatch) { - // Main title (bold) - const mainTitle = dom.$('span.chat-question-title-main'); - mainTitle.textContent = parenMatch[1]; - title.appendChild(mainTitle); - - // Subtitle in parentheses (normal weight) - const subtitle = dom.$('span.chat-question-title-subtitle'); - subtitle.textContent = ' ' + parenMatch[2]; - title.appendChild(subtitle); + if (isMarkdownString(questionText)) { + const renderedTitle = questionRenderStore.add(this._markdownRendererService.render(MarkdownString.lift(questionText))); + title.appendChild(renderedTitle.element); } else { - title.textContent = messageContent; + // Check for subtitle in parentheses at the end + const parenMatch = messageContent.match(/^(.+?)\s*(\([^)]+\))\s*$/); + if (parenMatch) { + // Main title (bold) + const mainTitle = dom.$('span.chat-question-title-main'); + mainTitle.textContent = parenMatch[1]; + title.appendChild(mainTitle); + + // Subtitle in parentheses (normal weight) + const subtitle = dom.$('span.chat-question-title-subtitle'); + subtitle.textContent = ' ' + parenMatch[2]; + title.appendChild(subtitle); + } else { + title.textContent = messageContent; + } } headerRow.appendChild(title); } @@ -1082,7 +1094,9 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent private renderSummary(): void { // If no answers, show skipped message if (this._answers.size === 0) { - this.renderSkippedMessage(); + if (this.carousel.isUsed) { + this.renderSkippedMessage(); + } return; } @@ -1178,6 +1192,14 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent } } + private getQuestionText(questionText: string | IMarkdownString): string { + if (typeof questionText === 'string') { + return questionText; + } + + return renderAsPlaintext(questionText); + } + 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/chatTipContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts index 7a9859e7a5837..256999f36bd9b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts @@ -66,7 +66,7 @@ export class ChatTipContentPart extends Disposable { const nextTip = this._getNextTip(); if (nextTip) { this._renderTip(nextTip); - this.focus(); + dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.domNode), () => this.focus()); } else { this._onDidHide.fire(); } @@ -74,7 +74,7 @@ export class ChatTipContentPart extends Disposable { this._register(this._chatTipService.onDidNavigateTip(tip => { this._renderTip(tip); - this.focus(); + dom.runAtThisOrScheduleAtNextAnimationFrame(dom.getWindow(this.domNode), () => this.focus()); })); this._register(this._chatTipService.onDidHideTip(() => { 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 4a6cbd669af0f..57a3a06ca786b 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 @@ -60,6 +60,21 @@ font-size: var(--vscode-chat-font-size-body-s); margin: 0; + .rendered-markdown { + a { + color: var(--vscode-textLink-foreground); + } + + a:hover, + a:active { + color: var(--vscode-textLink-activeForeground); + } + + p { + margin: 0; + } + } + .chat-question-title-main { font-weight: 500; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css index 33cd5d5783163..43832fd58ce10 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css @@ -59,7 +59,7 @@ background-color: var(--vscode-editorWidget-background); border-radius: 4px 4px 0 0; border: 1px solid var(--vscode-editorWidget-border, var(--vscode-input-border, transparent)); - border-bottom: none; /* Seamless attachment to input below */ + border-bottom: 1px solid var(--vscode-editorWidget-border, var(--vscode-input-border, transparent)); font-size: var(--vscode-chat-font-size-body-s); font-family: var(--vscode-chat-font-family, inherit); color: var(--vscode-descriptionForeground); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 8fb98c9147f1e..37c8ecfc743e9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -2103,6 +2103,13 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + const modelIdentifier = this.inputPart.selectedLanguageModel.read(reader)?.identifier; + if (previousModelIdentifier === undefined) { + previousModelIdentifier = modelIdentifier; + return; + } + + if (previousModelIdentifier === modelIdentifier) { + return; + } + + previousModelIdentifier = modelIdentifier; + if (!this._gettingStartedTipPartRef) { + return; + } + + this.chatTipService.getWelcomeTip(this.contextKeyService); + })); this._register(autorun(r => { const toolSetIds = new Set(); @@ -1991,17 +2011,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this.container.setAttribute('data-session-id', model.sessionId); this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection, undefined); - // mark any question carousels as used on reload - for (const request of model.getRequests()) { - if (request.response) { - for (const part of request.response.entireResponse.value) { - if (part.kind === 'questionCarousel' && !part.isUsed) { - part.isUsed = true; - } - } - } - } - // Pass input model reference to input part for state syncing this.inputPart.setInputModel(model.inputModel, model.getRequests().length === 0); this.listWidget.setViewModel(this.viewModel); @@ -2047,12 +2056,19 @@ export class ChatWidget extends Disposable implements IChatWidget { this.onDidChangeItems(); })); this._sessionIsEmptyContextKey.set(model.getRequests().length === 0); - const updatePendingRequestKeys = () => { - const pendingCount = model.getPendingRequests().length; + let lastSteeringCount = 0; + const updatePendingRequestKeys = (announceSteering: boolean) => { + const pendingRequests = model.getPendingRequests(); + const pendingCount = pendingRequests.length; this._hasPendingRequestsContextKey.set(pendingCount > 0); + const steeringCount = pendingRequests.filter(pending => pending.kind === ChatRequestQueueKind.Steering).length; + if (announceSteering && steeringCount > 0 && lastSteeringCount === 0) { + status(localize('chat.pendingRequests.steeringQueued', "Steering")); + } + lastSteeringCount = steeringCount; }; - updatePendingRequestKeys(); - this.viewModelDisposables.add(model.onDidChangePendingRequests(() => updatePendingRequestKeys())); + updatePendingRequestKeys(false); + this.viewModelDisposables.add(model.onDidChangePendingRequests(() => updatePendingRequestKeys(true))); this.refreshParsedInput(); this.viewModelDisposables.add(model.onDidChange((e) => { @@ -2263,18 +2279,23 @@ export class ChatWidget extends Disposable implements IChatWidget { return; } - const responseId = this.input.questionCarouselResponseId; + const inputPart = this.inputPartDisposable.value; + if (!inputPart) { + return; + } + + const responseId = inputPart.questionCarouselResponseId; if (!responseId || this.viewModel.model.lastRequest?.id !== responseId) { return; } - const carouselPart = this.input.questionCarousel; + const carouselPart = inputPart.questionCarousel; if (!carouselPart) { return; } carouselPart.ignore(); - this.input.clearQuestionCarousel(responseId); + inputPart.clearQuestionCarousel(responseId); } private async _acceptInput(query: { query: string } | undefined, options: IChatAcceptInputOptions = {}): Promise { 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 8800e0f570dd6..f1cd97d48fb66 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -103,7 +103,7 @@ import { ChatAttachmentModel } from '../../attachments/chatAttachmentModel.js'; import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, TerminalCommandAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../../attachments/chatAttachmentWidgets.js'; import { ChatImplicitContexts } from '../../attachments/chatImplicitContext.js'; import { ImplicitContextAttachmentWidget } from '../../attachments/implicitContextAttachment.js'; -import { IChatWidget, ISessionTypePickerDelegate, isIChatResourceViewContext, isIChatViewViewContext, IWorkspacePickerDelegate } from '../../chat.js'; +import { IChatWidget, IChatWidgetViewModelChangeEvent, ISessionTypePickerDelegate, isIChatResourceViewContext, isIChatViewViewContext, IWorkspacePickerDelegate } from '../../chat.js'; import { ChatEditingShowChangesAction, ViewAllSessionChangesAction, ViewPreviousEditsAction } from '../../chatEditing/chatEditingActions.js'; import { resizeImage } from '../../chatImageUtils.js'; import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from '../../chatSessions/chatSessionPickerActionItem.js'; @@ -209,6 +209,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private readonly _chatQuestionCarouselWidget = this._register(new MutableDisposable()); private readonly _chatQuestionCarouselDisposables = this._register(new DisposableStore()); private _currentQuestionCarouselResponseId: string | undefined; + private _currentQuestionCarouselSessionResource: URI | undefined; + private _hasQuestionCarouselContextKey: IContextKey | undefined; private readonly _chatEditingTodosDisposables = this._register(new DisposableStore()); private _lastEditingSessionResource: URI | undefined; @@ -347,6 +349,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private editingSentRequestKey!: IContextKey; private chatModeKindKey: IContextKey; private chatModeNameKey: IContextKey; + private chatModelIdKey: IContextKey; private withinEditSessionKey: IContextKey; private filePartOfEditSessionKey: IContextKey; private chatSessionHasOptions: IContextKey; @@ -562,8 +565,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.inputEditorHasText = ChatContextKeys.inputHasText.bindTo(contextKeyService); this.chatCursorAtTop = ChatContextKeys.inputCursorAtTop.bindTo(contextKeyService); this.inputEditorHasFocus = ChatContextKeys.inputHasFocus.bindTo(contextKeyService); + this._hasQuestionCarouselContextKey = ChatContextKeys.Editing.hasQuestionCarousel.bindTo(contextKeyService); this.chatModeKindKey = ChatContextKeys.chatModeKind.bindTo(contextKeyService); this.chatModeNameKey = ChatContextKeys.chatModeName.bindTo(contextKeyService); + this.chatModelIdKey = ChatContextKeys.chatModelId.bindTo(contextKeyService); this.withinEditSessionKey = ChatContextKeys.withinEditSessionDiff.bindTo(contextKeyService); this.filePartOfEditSessionKey = ChatContextKeys.filePartOfEditSession.bindTo(contextKeyService); this.chatSessionHasOptions = ChatContextKeys.chatSessionHasModels.bindTo(contextKeyService); @@ -635,6 +640,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })); this._register(autorun(reader => { const lm = this._currentLanguageModel.read(reader); + this.chatModelIdKey.set(lm?.metadata.id.toLowerCase() ?? ''); if (lm?.metadata.name) { this.accessibilityService.alert(lm.metadata.name); } @@ -1895,14 +1901,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - this._register(widget.onDidChangeViewModel(() => { + this._register(widget.onDidChangeViewModel((e: IChatWidgetViewModelChangeEvent) => { this._pendingDelegationTarget = undefined; // Update agentSessionType when view model changes this.updateAgentSessionTypeContextKey(); this.refreshChatSessionPickers(); this.tryUpdateWidgetController(); this.updateContextUsageWidget(); - this.clearQuestionCarousel(); + if (this._currentQuestionCarouselSessionResource && (!e.currentSessionResource || !isEqual(this._currentQuestionCarouselSessionResource, e.currentSessionResource))) { + this.clearQuestionCarousel(); + } // Track the current session type and re-initialize model selection // when the session type changes (different session types may have @@ -2609,11 +2617,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // track the response id and session this._currentQuestionCarouselResponseId = isResponseVM(context.element) ? context.element.requestId : undefined; + this._currentQuestionCarouselSessionResource = isResponseVM(context.element) ? context.element.sessionResource : undefined; const part = this._chatQuestionCarouselDisposables.add( this.instantiationService.createInstance(ChatQuestionCarouselPart, carousel, context, options) ); this._chatQuestionCarouselWidget.value = part; + this._hasQuestionCarouselContextKey?.set(true); dom.clearNode(this.chatQuestionCarouselContainer); dom.append(this.chatQuestionCarouselContainer, part.domNode); @@ -2628,6 +2638,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._chatQuestionCarouselDisposables.clear(); this._chatQuestionCarouselWidget.clear(); this._currentQuestionCarouselResponseId = undefined; + this._currentQuestionCarouselSessionResource = undefined; + this._hasQuestionCarouselContextKey?.set(false); dom.clearNode(this.chatQuestionCarouselContainer); } get questionCarouselResponseId(): string | undefined { 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 5d5b59ccca8fd..fac726fe312a2 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatQueuePickerActionItem.ts @@ -61,7 +61,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.send), + ThemeIcon.asClassName(Codicon.arrowUp), !!contextKeyService.getContextKeyValue(ChatContextKeys.inputHasText.key), () => this._runDefaultAction() )); @@ -194,7 +194,7 @@ export class ChatQueuePickerActionItem extends BaseActionViewItem { label: localize('chat.sendImmediately', "Stop and Send"), tooltip: '', enabled: true, - icon: Codicon.send, + icon: Codicon.arrowUp, class: undefined, hover: { content: localize('chat.sendImmediately.hover', "Cancel the current request and send this message immediately."), diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatStatusWidget.ts index 03954d815ded7..87fa84c517cbf 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatStatusWidget.ts @@ -51,10 +51,7 @@ export class ChatStatusWidget extends Disposable implements IChatInputPartWidget const entitlement = this.chatEntitlementService.entitlement; const isAnonymous = this.chatEntitlementService.anonymous; - // Free tier is always enabled, anonymous is controlled by experiment via chat.statusWidget.sku - const enabledSku = this.configurationService.getValue('chat.statusWidget.sku'); - - if (isAnonymous && enabledSku === 'anonymous') { + if (isAnonymous && this.configurationService.getValue('chat.statusWidget.anonymous')) { this.createWidgetContent('anonymous'); } else if (entitlement === ChatEntitlement.Free) { this.createWidgetContent('free'); @@ -82,7 +79,7 @@ export class ChatStatusWidget extends Disposable implements IChatInputPartWidget this.actionButton.element.classList.add('chat-status-button'); if (enabledSku === 'anonymous') { - const message = localize('chat.anonymousRateLimited.message', "You've reached the limit for chat messages. Try Copilot Pro for free."); + const message = localize('chat.anonymousRateLimited.message', "You've reached the limit for chat messages. Sign in to use Copilot Free."); const buttonLabel = localize('chat.anonymousRateLimited.signIn', "Sign In"); this.messageElement.textContent = message; this.actionButton.label = buttonLabel; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index e9dc1e75178ef..b429dfde84a39 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -10,7 +10,7 @@ import { coalesce } from '../../../../../../base/common/arrays.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { groupBy } from '../../../../../../base/common/collections.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; -import { autorun, IObservable } from '../../../../../../base/common/observable.js'; +import { autorun, IObservable, observableValue } from '../../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { URI } from '../../../../../../base/common/uri.js'; import { localize } from '../../../../../../nls.js'; @@ -33,6 +33,7 @@ import { getOpenChatActionIdForMode } from '../../actions/chatActions.js'; import { IToggleChatModeArgs, ToggleAgentModeActionId } from '../../actions/chatExecuteActions.js'; import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; +import { IWorkbenchAssignmentService } from '../../../../../services/assignment/common/assignmentService.js'; export interface IModePickerDelegate { readonly currentMode: IObservable; @@ -69,8 +70,11 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { @ICommandService commandService: ICommandService, @IProductService private readonly _productService: IProductService, @ITelemetryService telemetryService: ITelemetryService, - @IOpenerService openerService: IOpenerService + @IOpenerService openerService: IOpenerService, + @IWorkbenchAssignmentService assignmentService: IWorkbenchAssignmentService, ) { + const assignments = observableValue<{ showOldAskMode: boolean }>('modePickerAssignments', { showOldAskMode: false }); + // Get custom agent target (if filtering is enabled) const customAgentTarget = delegate.customAgentTarget?.() ?? Target.Undefined; @@ -210,20 +214,17 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { const agentMode = modes.builtin.find(mode => mode.id === ChatMode.Agent.id); const otherBuiltinModes = modes.builtin.filter(mode => { - if (mode.id === ChatMode.Agent.id) { - return false; - } - if (mode.id === ChatMode.Edit.id) { - return false; - } - if (mode.id === ChatMode.Ask.id) { - return false; + return mode.id !== ChatMode.Agent.id && shouldShowBuiltInMode(mode, assignments.get()); + }); + const filteredCustomModes = modes.custom.filter(mode => { + if (isModeConsideredBuiltIn(mode, this._productService)) { + return shouldShowBuiltInMode(mode, assignments.get()); } return true; }); // Filter out 'implement' mode from the dropdown - it's available for handoffs but not user-selectable const customModes = groupBy( - modes.custom, + filteredCustomModes, mode => isModeConsideredBuiltIn(mode, this._productService) ? 'builtin' : 'custom'); const modeSupportsVSCode = (mode: IChatMode) => { @@ -269,6 +270,13 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { this.renderLabel(this.element); } })); + + assignmentService.getTreatment('chat.showOldAskMode').then(showOldAskMode => { + assignments.set({ showOldAskMode: showOldAskMode === 'enabled' }, undefined); + }); + this._register(assignmentService.onDidRefetchAssignments(async () => { + assignments.set({ showOldAskMode: await assignmentService.getTreatment('chat.showOldAskMode') === 'enabled' }, undefined); + })); } private getModePickerActionBarActions(): IAction[] { @@ -326,3 +334,21 @@ function isModeConsideredBuiltIn(mode: IChatMode, productService: IProductServic } return !isOrganizationPromptFile(modeUri, mode.source.extensionId, productService); } + +function shouldShowBuiltInMode(mode: IChatMode, assignments: { showOldAskMode: boolean }): boolean { + // The built-in "Edit" mode is deprecated, but still supported for older conversations. + if (mode.id === ChatMode.Edit.id) { + return false; + } + + // The "Ask" mode is a special case - we want to show either the old or new version based on the assignment, but not both + // We still support the old "Ask" mode for conversations that already use it. + if (mode.id === ChatMode.Ask.id) { + return assignments.showOldAskMode; + } + if (mode.name.get().toLowerCase() === 'ask') { + return !assignments.showOldAskMode; + } + + return true; +} 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 c0abfa14e9703..4580ea562d725 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -795,12 +795,12 @@ have to be updated for changes to the rules above, or to support more deeply nes margin-right: 3px; } -.interactive-session .chat-input-container { +.monaco-workbench .interactive-session .chat-input-container { box-sizing: border-box; cursor: text; background-color: var(--vscode-input-background); border: 1px solid var(--vscode-input-border, transparent); - border-radius: 4px; + border-radius: var(--vscode-cornerRadius-large); padding: 0 6px 6px 6px; /* top padding is inside the editor widget */ width: 100%; @@ -837,13 +837,13 @@ have to be updated for changes to the rules above, or to support more deeply nes position: relative; } -.interactive-session .chat-editing-session .chat-editing-session-container { +.monaco-workbench .interactive-session .chat-editing-session .chat-editing-session-container { padding: 4px 3px 4px 3px; box-sizing: border-box; background-color: var(--vscode-editor-background); border: 1px solid var(--vscode-input-border, transparent); border-bottom: none; - border-radius: 4px 4px 0 0; + border-radius: var(--vscode-cornerRadius-large) var(--vscode-cornerRadius-large) 0 0; display: flex; flex-direction: column; gap: 2px; @@ -937,7 +937,7 @@ have to be updated for changes to the rules above, or to support more deeply nes border-radius: 2px; border: none; background-color: unset; - color: var(--vscode-foreground) + color: var(--vscode-descriptionForeground); } .monaco-button:focus-visible { @@ -1300,6 +1300,10 @@ have to be updated for changes to the rules above, or to support more deeply nes padding-left: 4px; } +.interactive-session .chat-editor-container .monaco-editor .view-lines { + padding-left: 4px; +} + .interactive-session .chat-input-toolbars { display: flex; } @@ -1371,11 +1375,16 @@ have to be updated for changes to the rules above, or to support more deeply nes padding: 3px 0px 3px 6px; display: flex; align-items: center; + color: var(--vscode-descriptionForeground); } +.monaco-workbench .interactive-session .chat-input-toolbars .monaco-action-bar .action-item .codicon.codicon, +.monaco-workbench .interactive-session .chat-input-toolbars .action-label .codicon.codicon { + color: var(--vscode-descriptionForeground) !important; +} -.interactive-session .chat-input-toolbar .chat-input-picker-item .action-label .codicon-chevron-down, -.interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label .codicon-chevron-down { +.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; } 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 1435641c9ebaa..df7195252c14a 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -663,7 +663,12 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { //#region Model Management - private async applyModel(): Promise { + private applyModel(): void { + this.restoringSession = this._applyModel(); + this.restoringSession.finally(() => this.restoringSession = undefined); + } + + private async _applyModel(): Promise { const sessionResource = this.getTransferredOrPersistedSessionInfo(); const modelRef = sessionResource ? await this.chatService.getOrRestoreSession(sessionResource) : undefined; await this.showModel(modelRef); @@ -748,6 +753,12 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { } async loadSession(sessionResource: URI): Promise { + // Wait for any in-progress session restore (e.g. from onDidChangeAgents) + // to finish first, so our showModel call is guaranteed to be the last one. + if (this.restoringSession) { + await this.restoringSession; + } + return this.progressService.withProgress({ location: ChatViewId, delay: 200 }, async () => { let queue: Promise = Promise.resolve(); diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index 5e45b28b9de6b..b97209bce1587 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -45,6 +45,7 @@ export namespace ChatContextKeys { 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 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').") }); export const supported = ContextKeyExpr.or(IsWebContext.negate(), RemoteNameContext.notEqualsTo(''), ContextKeyExpr.has('config.chat.experimental.serverlessWebEnabled')); export const enabled = new RawContextKey('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is activated with an implementation.") }); @@ -102,6 +103,7 @@ export namespace ChatContextKeys { export const Editing = { hasToolConfirmation: new RawContextKey('chatHasToolConfirmation', false, { type: 'boolean', description: localize('chatEditingHasToolConfirmation', "True when a tool confirmation is present.") }), hasElicitationRequest: new RawContextKey('chatHasElicitationRequest', false, { type: 'boolean', description: localize('chatEditingHasElicitationRequest', "True when a chat elicitation request is pending.") }), + hasQuestionCarousel: new RawContextKey('chatHasQuestionCarousel', false, { type: 'boolean', description: localize('chatEditingHasQuestionCarousel', "True when a question carousel is rendered in the chat input.") }), }; export const Tools = { diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index acdc50bb5a99e..2351ac5537c4d 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -28,6 +28,7 @@ import { Progress } from '../../../../../platform/progress/common/progress.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; +import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; import { InlineChatConfigKeys } from '../../../inlineChat/common/inlineChat.js'; import { IMcpService } from '../../../mcp/common/mcpTypes.js'; import { awaitStatsForSession } from '../chat.js'; @@ -155,6 +156,7 @@ export class ChatService extends Disposable implements IChatService { @IChatSessionsService private readonly chatSessionService: IChatSessionsService, @IMcpService private readonly mcpService: IMcpService, @IPromptsService private readonly promptsService: IPromptsService, + @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, ) { super(); @@ -340,7 +342,17 @@ export class ChatService extends Disposable implements IChatService { return; } - const sessionResource = LocalChatSessionUri.forSession(session.sessionId); + let sessionResource: URI; + // Non-local sessions store the full uri as the sessionId, so try parsing that first + if (session.sessionId.includes(':')) { + try { + sessionResource = URI.parse(session.sessionId, true); + } catch { + // Noop + } + } + sessionResource ??= LocalChatSessionUri.forSession(session.sessionId); + const sessionRef = await this.getOrRestoreSession(sessionResource); if (sessionRef?.object.editingSession) { await chatEditingSessionIsReady(sessionRef.object.editingSession); @@ -1128,6 +1140,10 @@ export class ChatService extends Disposable implements IChatService { completeResponseCreated(); this.trace('sendRequest', `Provider returned response for session ${model.sessionResource}`); + if (rawResult.errorDetails?.isRateLimited) { + this.chatEntitlementService.markAnonymousRateLimited(); + } + shouldProcessPending = !rawResult.errorDetails && !token.isCancellationRequested; request.response?.complete(); if (agentOrCommandFollowups) { diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index 989e005c0b5fb..8ccd21ac64711 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -262,7 +262,7 @@ export interface IChatSessionsService { /** * Fired when options for a chat session change. */ - onDidChangeSessionOptions: Event; + readonly onDidChangeSessionOptions: Event; /** * Get the capabilities for a specific session type @@ -279,7 +279,7 @@ export interface IChatSessionsService { * Returns whether the session type requires custom models. When true, the model picker should show filtered custom models. */ requiresCustomModelsForSessionType(chatSessionType: string): boolean; - onDidChangeOptionGroups: Event; + readonly onDidChangeOptionGroups: Event; getOptionGroupsForSessionType(chatSessionType: string): IChatSessionProviderOptionGroup[] | undefined; setOptionGroupsForSessionType(chatSessionType: string, handle: number, optionGroups?: IChatSessionProviderOptionGroup[]): void; diff --git a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts index 28f9b609fa15e..5ee2250a37324 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts @@ -7,14 +7,14 @@ import assert from 'assert'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ICommandEvent, ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { ConfigurationTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; -import { IStorageService, InMemoryStorageService } from '../../../../../platform/storage/common/storage.js'; +import { IStorageService, InMemoryStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { ChatTipService, ITipDefinition, TipEligibilityTracker } from '../../browser/chatTipService.js'; import { AgentFileType, IPromptPath, IPromptsService, IResolvedAgentFile, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -30,6 +30,19 @@ class MockContextKeyServiceWithRulesMatching extends MockContextKeyService { } } +class TrackingConfigurationService extends TestConfigurationService { + public lastUpdateTarget: ConfigurationTarget | undefined; + public lastUpdateKey: string | undefined; + public lastUpdateValue: unknown; + + override updateValue(key: string, value: unknown, arg3?: unknown): Promise { + this.lastUpdateKey = key; + this.lastUpdateValue = value; + this.lastUpdateTarget = arg3 as ConfigurationTarget | undefined; + return Promise.resolve(undefined); + } +} + suite('ChatTipService', () => { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -87,6 +100,73 @@ suite('ChatTipService', () => { assert.ok(tip.content.value.length > 0, 'Tip should have content'); }); + test('returns Auto switch tip when current model is gpt-4.1', () => { + const service = createService(); + contextKeyService.createKey(ChatContextKeys.chatModelId.key, 'gpt-4.1'); + + const tip = service.getWelcomeTip(contextKeyService); + + assert.ok(tip); + assert.strictEqual(tip.id, 'tip.switchToAuto'); + }); + + test('does not return Auto switch tip when current model is not gpt-4.1', () => { + const service = createService(); + contextKeyService.createKey(ChatContextKeys.chatModelId.key, 'auto'); + + const tip = service.getWelcomeTip(contextKeyService); + + assert.ok(tip); + assert.notStrictEqual(tip.id, 'tip.switchToAuto'); + }); + + test('does not return Auto switch tip when current model context key is empty and no fallback is available', () => { + const service = createService(); + contextKeyService.createKey(ChatContextKeys.chatModelId.key, ''); + + const tip = service.getWelcomeTip(contextKeyService); + + assert.ok(tip); + assert.notStrictEqual(tip.id, 'tip.switchToAuto'); + }); + + test('returns Auto switch tip when current model is persisted and context key is empty', () => { + storageService.store('chat.currentLanguageModel.panel', 'copilot/gpt-4.1-2025-04-14', StorageScope.APPLICATION, StorageTarget.USER); + const service = createService(); + contextKeyService.createKey(ChatContextKeys.chatModelId.key, ''); + + const tip = service.getWelcomeTip(contextKeyService); + + assert.ok(tip); + assert.strictEqual(tip.id, 'tip.switchToAuto'); + }); + + test('returns Auto switch tip when current model is versioned gpt-4.1', () => { + const service = createService(); + contextKeyService.createKey(ChatContextKeys.chatModelId.key, 'gpt-4.1-2025-04-14'); + + const tip = service.getWelcomeTip(contextKeyService); + + assert.ok(tip); + assert.strictEqual(tip.id, 'tip.switchToAuto'); + }); + + test('switching models advances away from gpt-4.1 tip', () => { + const service = createService(); + contextKeyService.createKey(ChatContextKeys.chatModelId.key, 'gpt-4.1'); + + const firstTip = service.getWelcomeTip(contextKeyService); + assert.ok(firstTip); + assert.strictEqual(firstTip.id, 'tip.switchToAuto'); + + const switchedContextKeyService = new MockContextKeyServiceWithRulesMatching(); + switchedContextKeyService.createKey(ChatContextKeys.chatModelId.key, 'auto'); + const nextTip = service.getWelcomeTip(switchedContextKeyService); + + assert.ok(nextTip); + assert.notStrictEqual(nextTip.id, 'tip.switchToAuto'); + }); + test('returns same welcome tip on rerender', () => { const service = createService(); @@ -171,6 +251,20 @@ suite('ChatTipService', () => { assert.ok(fired, 'onDidDisableTips should fire'); }); + test('disableTips writes to application settings target', async () => { + const trackingConfigurationService = new TrackingConfigurationService(); + configurationService = trackingConfigurationService; + instantiationService.stub(IConfigurationService, configurationService); + + const service = createService(); + + await service.disableTips(); + + assert.strictEqual(trackingConfigurationService.lastUpdateKey, 'chat.tips.enabled'); + assert.strictEqual(trackingConfigurationService.lastUpdateValue, false); + assert.strictEqual(trackingConfigurationService.lastUpdateTarget, ConfigurationTarget.APPLICATION); + }); + test('disableTips resets state so re-enabling works', async () => { const service = createService(); 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 4cffb9f84e7df..2d91102c1e1b0 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 @@ -5,6 +5,7 @@ import assert from 'assert'; import { mainWindow } from '../../../../../../../base/browser/window.js'; +import { MarkdownString } from '../../../../../../../base/common/htmlContent.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; import { ChatQuestionCarouselPart, IChatQuestionCarouselOptions } from '../../../../browser/widget/chatContentParts/chatQuestionCarouselPart.js'; @@ -85,6 +86,25 @@ suite('ChatQuestionCarouselPart', () => { assert.ok(title?.textContent?.includes('Fallback title text')); }); + test('renders markdown in question message', () => { + const carousel = createMockCarousel([ + { + id: 'q1', + type: 'text', + title: 'Question', + message: new MarkdownString('Please review **details** in [docs](https://example.com)') + } + ]); + createWidget(carousel); + + const title = widget.domNode.querySelector('.chat-question-title'); + assert.ok(title, 'title element should exist'); + assert.ok(title?.querySelector('.rendered-markdown'), 'markdown content should be rendered'); + assert.strictEqual(title?.textContent?.includes('**details**'), false, 'markdown syntax should not be shown as raw text'); + const link = title?.querySelector('a') as HTMLAnchorElement | null; + assert.ok(link, 'markdown link should render as anchor'); + }); + test('renders progress indicator correctly', () => { const carousel = createMockCarousel([ { id: 'q1', type: 'text', title: 'Question 1', message: 'Question 1' }, diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts index 601e138974197..15a5add31964f 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts @@ -33,7 +33,8 @@ import { IExtensionService, nullExtensionDescription } from '../../../../../serv import { ILifecycleService } from '../../../../../services/lifecycle/common/lifecycle.js'; import { IViewsService } from '../../../../../services/views/common/viewsService.js'; import { IWorkspaceEditingService } from '../../../../../services/workspaces/common/workspaceEditing.js'; -import { InMemoryTestFileService, mock, TestContextService, TestExtensionService, TestStorageService } from '../../../../../test/common/workbenchTestServices.js'; +import { IChatEntitlementService } from '../../../../../services/chat/common/chatEntitlementService.js'; +import { InMemoryTestFileService, mock, TestChatEntitlementService, TestContextService, TestExtensionService, TestStorageService } from '../../../../../test/common/workbenchTestServices.js'; import { IMcpService } from '../../../../mcp/common/mcpTypes.js'; import { TestMcpService } from '../../../../mcp/test/common/testMcpService.js'; import { ChatAgentService, IChatAgent, IChatAgentData, IChatAgentImplementation, IChatAgentService } from '../../../common/participants/chatAgents.js'; @@ -163,6 +164,7 @@ suite('ChatService', () => { [IPromptsService, new MockPromptsService()], ))); instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); + instantiationService.stub(IChatEntitlementService, new TestChatEntitlementService()); instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IUserDataProfilesService, { defaultProfile: toUserDataProfile('default', 'Default', URI.file('/test/userdata'), URI.file('/test/cache')) }); instantiationService.stub(ITelemetryService, NullTelemetryService); diff --git a/src/vs/workbench/contrib/terminal/browser/remotePty.ts b/src/vs/workbench/contrib/terminal/browser/remotePty.ts index e27137d903c48..68d16e842f824 100644 --- a/src/vs/workbench/contrib/terminal/browser/remotePty.ts +++ b/src/vs/workbench/contrib/terminal/browser/remotePty.ts @@ -80,14 +80,14 @@ export class RemotePty extends BasePty implements ITerminalChildProcess { return this._remoteTerminalChannel.processBinary(this.id, e); } - resize(cols: number, rows: number): void { + resize(cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): void { if (this._inReplay || this._lastDimensions.cols === cols && this._lastDimensions.rows === rows) { return; } this._startBarrier.wait().then(_ => { this._lastDimensions.cols = cols; this._lastDimensions.rows = rows; - this._remoteTerminalChannel.resize(this.id, cols, rows); + this._remoteTerminalChannel.resize(this.id, cols, rows, pixelWidth, pixelHeight); }); } diff --git a/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts index e2abc0fe925f0..40335e7d4c778 100644 --- a/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts @@ -28,6 +28,7 @@ import { ICompleteTerminalConfiguration, ITerminalConfiguration, TERMINAL_CONFIG import { TerminalStorageKeys } from '../common/terminalStorageKeys.js'; import { IConfigurationResolverService } from '../../../services/configurationResolver/common/configurationResolver.js'; import { IHistoryService } from '../../../services/history/common/history.js'; +import { getWorkspaceForTerminal } from '../common/terminalEnvironment.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { IStatusbarService } from '../../../services/statusbar/browser/statusbar.js'; @@ -193,7 +194,7 @@ class RemoteTerminalBackend extends BaseTerminalBackend implements ITerminalBack tabActions: shellLaunchConfig.tabActions, shellIntegrationEnvironmentReporting: shellLaunchConfig.shellIntegrationEnvironmentReporting, }; - const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(); + const activeWorkspaceRootUri = getWorkspaceForTerminal(shellLaunchConfig.cwd, this._workspaceContextService, this._historyService)?.uri; const result = await this._remoteTerminalChannel.createProcess( shellLaunchConfigDto, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 7a71b3c59e170..a9fd028211a58 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -2006,7 +2006,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } private async _updatePtyDimensions(rawXterm: XTermTerminal): Promise { - await this._processManager.setDimensions(rawXterm.cols, rawXterm.rows); + const pixelWidth = rawXterm.dimensions?.css.canvas.width; + const pixelHeight = rawXterm.dimensions?.css.canvas.height; + const roundedPixelWidth = pixelWidth ? Math.round(pixelWidth) : undefined; + const roundedPixelHeight = pixelHeight ? Math.round(pixelHeight) : undefined; + await this._processManager.setDimensions(rawXterm.cols, rawXterm.rows, undefined, roundedPixelWidth, roundedPixelHeight); } setShellType(shellType: TerminalShellType | undefined) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 251a94fd3e65c..2cc3d43b763cc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -581,16 +581,16 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce return os; } - setDimensions(cols: number, rows: number): Promise; - setDimensions(cols: number, rows: number, sync: false): Promise; - setDimensions(cols: number, rows: number, sync: true): void; - setDimensions(cols: number, rows: number, sync?: boolean): MaybePromise { + setDimensions(cols: number, rows: number, sync?: undefined, pixelWidth?: number, pixelHeight?: number): Promise; + setDimensions(cols: number, rows: number, sync: false, pixelWidth?: number, pixelHeight?: number): Promise; + setDimensions(cols: number, rows: number, sync: true, pixelWidth?: number, pixelHeight?: number): void; + setDimensions(cols: number, rows: number, sync?: boolean, pixelWidth?: number, pixelHeight?: number): MaybePromise { if (sync) { - this._resize(cols, rows); + this._resize(cols, rows, pixelWidth, pixelHeight); return; } - return this.ptyProcessReady.then(() => this._resize(cols, rows)); + return this.ptyProcessReady.then(() => this._resize(cols, rows, pixelWidth, pixelHeight)); } async setUnicodeVersion(version: '6' | '11'): Promise { @@ -606,13 +606,13 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce await this._terminalService.setNextCommandId(process.id, commandLine, commandId); } - private _resize(cols: number, rows: number) { + private _resize(cols: number, rows: number, pixelWidth?: number, pixelHeight?: number) { if (!this._process) { return; } // The child process could already be terminated try { - this._process.resize(cols, rows); + this._process.resize(cols, rows, pixelWidth, pixelHeight); } catch (error) { // We tried to write to a closed pipe / channel. if (error.code !== 'EPIPE' && error.code !== 'ERR_IPC_CHANNEL_CLOSED') { diff --git a/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts index b728f908338de..6b6e849e3444d 100644 --- a/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts @@ -231,8 +231,8 @@ export class RemoteTerminalChannelClient implements IPtyHostController { shutdown(id: number, immediate: boolean): Promise { return this._channel.call(RemoteTerminalChannelRequest.Shutdown, [id, immediate]); } - resize(id: number, cols: number, rows: number): Promise { - return this._channel.call(RemoteTerminalChannelRequest.Resize, [id, cols, rows]); + resize(id: number, cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): Promise { + return this._channel.call(RemoteTerminalChannelRequest.Resize, [id, cols, rows, pixelWidth, pixelHeight]); } clearBuffer(id: number): Promise { return this._channel.call(RemoteTerminalChannelRequest.ClearBuffer, [id]); diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 19a1d971cdbd5..12fa676c314ed 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -300,9 +300,9 @@ export interface ITerminalProcessManager extends IDisposable, ITerminalProcessIn relaunch(shellLaunchConfig: IShellLaunchConfig, cols: number, rows: number, reset: boolean): Promise; write(data: string): Promise; sendSignal(signal: string): Promise; - setDimensions(cols: number, rows: number): Promise; - setDimensions(cols: number, rows: number, sync: false): Promise; - setDimensions(cols: number, rows: number, sync: true): void; + setDimensions(cols: number, rows: number, sync?: undefined, pixelWidth?: number, pixelHeight?: number): Promise; + setDimensions(cols: number, rows: number, sync: false, pixelWidth?: number, pixelHeight?: number): Promise; + setDimensions(cols: number, rows: number, sync: true, pixelWidth?: number, pixelHeight?: number): void; clearBuffer(): Promise; setUnicodeVersion(version: '6' | '11'): Promise; setNextCommandId(commandLine: string, commandId: string): Promise; diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index e97facbaa3541..a99f9628499c4 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -342,7 +342,7 @@ const terminalConfiguration: IStringDictionary = { }, [TerminalSettingId.TerminalTitle]: { 'type': 'string', - 'default': '${sequence}', + 'default': '${process}', 'markdownDescription': terminalTitle }, [TerminalSettingId.TerminalDescription]: { diff --git a/src/vs/workbench/contrib/terminal/electron-browser/localPty.ts b/src/vs/workbench/contrib/terminal/electron-browser/localPty.ts index 6405af520543b..eed9c7fa8e109 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/localPty.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/localPty.ts @@ -52,13 +52,13 @@ export class LocalPty extends BasePty implements ITerminalChildProcess { this._proxy.sendSignal(this.id, signal); } - resize(cols: number, rows: number): void { + resize(cols: number, rows: number, pixelWidth?: number, pixelHeight?: number): void { if (this._inReplay || this._lastDimensions.cols === cols && this._lastDimensions.rows === rows) { return; } this._lastDimensions.cols = cols; this._lastDimensions.rows = rows; - this._proxy.resize(this.id, cols, rows); + this._proxy.resize(this.id, cols, rows, pixelWidth, pixelHeight); } async clearBuffer(): Promise { diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts index dad6191105932..f06292f5e1d84 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts @@ -7,9 +7,11 @@ import { deepStrictEqual, strictEqual } from 'assert'; import { IStringDictionary } from '../../../../../base/common/collections.js'; import { isWindows, OperatingSystem } from '../../../../../base/common/platform.js'; import { URI as Uri } from '../../../../../base/common/uri.js'; -import { addTerminalEnvironmentKeys, createTerminalEnvironment, getUriLabelForShell, getCwd, getLangEnvVariable, mergeEnvironments, preparePathForShell, shouldSetLangEnvVariable } from '../../common/terminalEnvironment.js'; +import { addTerminalEnvironmentKeys, createTerminalEnvironment, getUriLabelForShell, getCwd, getLangEnvVariable, getWorkspaceForTerminal, mergeEnvironments, preparePathForShell, shouldSetLangEnvVariable } from '../../common/terminalEnvironment.js'; import { GeneralShellType, PosixShellType, WindowsShellType } from '../../../../../platform/terminal/common/terminal.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { TestContextService, TestHistoryService } from '../../../../test/common/workbenchTestServices.js'; +import { testWorkspace } from '../../../../../platform/workspace/test/common/testWorkspace.js'; const wslPathBackend = { getWslPath: async (original: string, direction: 'unix-to-win' | 'win-to-unix') => { @@ -320,6 +322,38 @@ suite('Workbench - TerminalEnvironment', () => { ); }); }); + suite('getWorkspaceForTerminal', () => { + test('should resolve workspace folder from cwd, not last active workspace', () => { + const folderA = Uri.file('/workspace/proj1'); + const folderB = Uri.file('/workspace/proj2'); + const contextService = new TestContextService(testWorkspace(folderA, folderB)); + const historyService = new TestHistoryService(folderA); + const result = getWorkspaceForTerminal(folderB, contextService, historyService); + strictEqual(result?.uri.fsPath, folderB.fsPath); + }); + + test('should fall back to last active workspace when cwd is not in any workspace folder', () => { + const folderA = Uri.file('/workspace/proj1'); + const contextService = new TestContextService(testWorkspace(folderA)); + const historyService = new TestHistoryService(folderA); + const result = getWorkspaceForTerminal(Uri.file('/other/path'), contextService, historyService); + strictEqual(result?.uri.fsPath, folderA.fsPath); + }); + + test('should fall back to last active workspace when cwd is undefined', () => { + const folderA = Uri.file('/workspace/proj1'); + const contextService = new TestContextService(testWorkspace(folderA)); + const historyService = new TestHistoryService(folderA); + strictEqual(getWorkspaceForTerminal(undefined, contextService, historyService)?.uri.fsPath, folderA.fsPath); + }); + + test('should return undefined when cwd and history are both unavailable', () => { + const contextService = new TestContextService(testWorkspace(Uri.file('/workspace/proj1'))); + const historyService = new TestHistoryService(undefined); + strictEqual(getWorkspaceForTerminal(undefined, contextService, historyService), undefined); + }); + }); + suite('formatUriForShellDisplay', () => { test('Wsl', async () => { strictEqual(await getUriLabelForShell('c:\\foo\\bar', wslPathBackend, WindowsShellType.Wsl, OperatingSystem.Windows, true), '/mnt/c/foo/bar'); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 90743dfac7fc7..89dc39f4b9637 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -478,7 +478,6 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: TerminalChatCommandId.FocusMostRecentChatTerminal, weight: KeybindingWeight.WorkbenchContrib, when: ChatContextKeys.inChatSession, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KeyT, handler: async (accessor: ServicesAccessor) => { const terminalChatService = accessor.get(ITerminalChatService); const part = terminalChatService.getMostRecentProgressPart(); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts index 9786f71a25c7a..eda700886bdc8 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts @@ -258,7 +258,8 @@ export async function collectTerminalResults( } } - const outputMonitor = disposableStore.add(instantiationService.createInstance(OutputMonitor, execution, taskProblemPollFn, invocationContext, token, task._label)); + const hasProblemMatchers = terminalTask.configurationProperties.problemMatchers && terminalTask.configurationProperties.problemMatchers.length > 0; + const outputMonitor = disposableStore.add(instantiationService.createInstance(OutputMonitor, execution, hasProblemMatchers ? taskProblemPollFn : undefined, invocationContext, token, task._label)); await Promise.race([ Event.toPromise(outputMonitor.onDidFinishCommand), Event.toPromise(token.onCancellationRequested as Event) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts index e624c80821c00..085c43ed463e9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts @@ -355,7 +355,7 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { const custom = await this._pollFn?.(this._execution, token, this._taskService); this._logService.trace(`OutputMonitor: Custom poller result: ${custom ? 'provided' : 'none'}`); const resources = custom?.resources; - const modelOutputEvalResponse = await this._assessOutputForErrors(this._execution.getOutput(), token); + const modelOutputEvalResponse = this._pollFn ? undefined : await this._assessOutputForErrors(this._execution.getOutput(), token); return { resources, modelOutputEvalResponse, shouldContinuePollling: false, output: custom?.output ?? output }; } diff --git a/src/vs/workbench/services/assignment/common/assignmentService.ts b/src/vs/workbench/services/assignment/common/assignmentService.ts index 9f3b69e0232a9..f7594b944a2d3 100644 --- a/src/vs/workbench/services/assignment/common/assignmentService.ts +++ b/src/vs/workbench/services/assignment/common/assignmentService.ts @@ -178,6 +178,11 @@ export class WorkbenchAssignmentService extends Disposable implements IAssignmen this.telemetry = this._register(new WorkbenchAssignmentServiceTelemetry(telemetryService, productService)); this._register(this.telemetry.onDidUpdateAssignmentContext(() => this._onDidRefetchAssignments.fire())); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('experiments.override')) { + this._onDidRefetchAssignments.fire(); + } + })); this.keyValueStorage = new MementoKeyValueStorage(new Memento>('experiment.service.memento', storageService)); diff --git a/src/vs/workbench/services/chat/common/chatEntitlementService.ts b/src/vs/workbench/services/chat/common/chatEntitlementService.ts index adca06c9c5aa8..919d79dc291ee 100644 --- a/src/vs/workbench/services/chat/common/chatEntitlementService.ts +++ b/src/vs/workbench/services/chat/common/chatEntitlementService.ts @@ -164,6 +164,8 @@ export interface IChatEntitlementService { readonly anonymous: boolean; readonly anonymousObs: IObservable; + markAnonymousRateLimited(): void; + update(token: CancellationToken): Promise; } @@ -511,6 +513,15 @@ export class ChatEntitlementService extends Disposable implements IChatEntitleme //#endregion + markAnonymousRateLimited(): void { + if (!this.anonymous) { + return; + } + + this.chatQuotaExceededContextKey.set(true); + this._onDidChangeQuotaExceeded.fire(); + } + async update(token: CancellationToken): Promise { await this.requests?.value.forceResolveEntitlement(token); } diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index 25ebf2bcc5281..e625aa79e5bbd 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -806,6 +806,8 @@ export class TestChatEntitlementService implements IChatEntitlementService { onDidChangeAnonymous = Event.None; readonly anonymousObs = observableValue({}, false); + markAnonymousRateLimited(): void { } + readonly previewFeaturesDisabled = false; } diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index 699ff90a850c2..9672580dfd896 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -118,9 +118,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", diff --git a/test/smoke/src/areas/chat/chatAnonymous.test.ts b/test/smoke/src/areas/chat/chatAnonymous.test.ts deleted file mode 100644 index 520292a96d4a7..0000000000000 --- a/test/smoke/src/areas/chat/chatAnonymous.test.ts +++ /dev/null @@ -1,37 +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 { Application, Logger } from '../../../../automation'; -import { installAllHandlers } from '../../utils'; - -export function setup(logger: Logger) { - describe.skip('Chat Anonymous', () => { - - // Shared before/after handling - installAllHandlers(logger); - - it('can send a chat message with anonymous access', async function () { - const app = this.app as Application; - - // Enable anonymous access - await app.workbench.settingsEditor.addUserSetting('chat.allowAnonymousAccess', 'true'); - - // Open chat view - await app.workbench.quickaccess.runCommand('workbench.action.chat.open'); - - // Wait for chat view to be visible - await app.workbench.chat.waitForChatView(); - - // Send a message - await app.workbench.chat.sendMessage('Hello'); - - // Wait for a response to complete - await app.workbench.chat.waitForResponse(); - - // Wait for model name to appear in footer - await app.workbench.chat.waitForModelInFooter(); - }); - }); -} diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index c57fbc25ecb03..db83970d3b1bb 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -28,7 +28,6 @@ import { setup as setupLaunchTests } from './areas/workbench/launch.test'; import { setup as setupTerminalTests } from './areas/terminal/terminal.test'; import { setup as setupTaskTests } from './areas/task/task.test'; import { setup as setupChatTests } from './areas/chat/chatDisabled.test'; -import { setup as setupChatAnonymousTests } from './areas/chat/chatAnonymous.test'; import { setup as setupAccessibilityTests } from './areas/accessibility/accessibility.test'; const rootPath = path.join(__dirname, '..', '..', '..'); @@ -419,6 +418,5 @@ describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { if (!opts.web && !opts.remote && quality !== Quality.Dev && quality !== Quality.OSS) { setupLocalizationTests(logger); } if (!opts.web && !opts.remote) { setupLaunchTests(logger); } if (!opts.web) { setupChatTests(logger); } - if (!opts.web && quality === Quality.Insiders) { setupChatAnonymousTests(logger); } setupAccessibilityTests(logger, opts, quality); });