diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 2aba62deea241..bc13d980df2dd 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -100,6 +100,23 @@ jobs: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Compile & Hygiene + - script: | + set -e + + [ -d "out-build" ] || { echo "ERROR: out-build folder is missing" >&2; exit 1; } + [ -n "$(find out-build -mindepth 1 2>/dev/null | head -1)" ] || { echo "ERROR: out-build folder is empty" >&2; exit 1; } + echo "out-build exists and is not empty" + + ls -d out-vscode-* >/dev/null 2>&1 || { echo "ERROR: No out-vscode-* folders found" >&2; exit 1; } + for folder in out-vscode-*; do + [ -d "$folder" ] || { echo "ERROR: $folder is missing" >&2; exit 1; } + [ -n "$(find "$folder" -mindepth 1 2>/dev/null | head -1)" ] || { echo "ERROR: $folder is empty" >&2; exit 1; } + echo "$folder exists and is not empty" + done + + echo "All required compilation folders checked." + displayName: Validate compilation folders + - script: | set -e npm run compile diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index 79802e7366821..8b361f66f61ae 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -51,6 +51,15 @@ "default": true, "title": "Focus Lock Indicator Enabled", "description": "%configuration.focusLockIndicator.enabled.description%" + }, + "simpleBrowser.useIntegratedBrowser": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.useIntegratedBrowser.description%", + "scope": "application", + "tags": [ + "experimental" + ] } } } diff --git a/extensions/simple-browser/package.nls.json b/extensions/simple-browser/package.nls.json index 496dc28dfddf4..3b6b41530fa04 100644 --- a/extensions/simple-browser/package.nls.json +++ b/extensions/simple-browser/package.nls.json @@ -1,5 +1,6 @@ { "displayName": "Simple Browser", "description": "A very basic built-in webview for displaying web content.", - "configuration.focusLockIndicator.enabled.description": "Enable/disable the floating indicator that shows when focused in the simple browser." + "configuration.focusLockIndicator.enabled.description": "Enable/disable the floating indicator that shows when focused in the simple browser.", + "configuration.useIntegratedBrowser.description": "When enabled, the `simpleBrowser.show` command will open URLs in the integrated browser instead of the Simple Browser webview. **Note:** This setting is experimental and only available on desktop." } diff --git a/extensions/simple-browser/src/extension.ts b/extensions/simple-browser/src/extension.ts index 885afe287121d..6eb0bb0837f11 100644 --- a/extensions/simple-browser/src/extension.ts +++ b/extensions/simple-browser/src/extension.ts @@ -14,6 +14,8 @@ declare class URL { const openApiCommand = 'simpleBrowser.api.open'; const showCommand = 'simpleBrowser.show'; +const integratedBrowserCommand = 'workbench.action.browser.open'; +const useIntegratedBrowserSetting = 'simpleBrowser.useIntegratedBrowser'; const enabledHosts = new Set([ 'localhost', @@ -31,6 +33,27 @@ const enabledHosts = new Set([ const openerId = 'simpleBrowser.open'; +/** + * Checks if the integrated browser should be used instead of the simple browser + */ +async function shouldUseIntegratedBrowser(): Promise { + const config = vscode.workspace.getConfiguration(); + if (!config.get(useIntegratedBrowserSetting, false)) { + return false; + } + + // Verify that the integrated browser command is available + const commands = await vscode.commands.getCommands(true); + return commands.includes(integratedBrowserCommand); +} + +/** + * Opens a URL in the integrated browser + */ +async function openInIntegratedBrowser(url?: string): Promise { + await vscode.commands.executeCommand(integratedBrowserCommand, url); +} + export function activate(context: vscode.ExtensionContext) { const manager = new SimpleBrowserManager(context.extensionUri); @@ -43,6 +66,10 @@ export function activate(context: vscode.ExtensionContext) { })); context.subscriptions.push(vscode.commands.registerCommand(showCommand, async (url?: string) => { + if (await shouldUseIntegratedBrowser()) { + return openInIntegratedBrowser(url); + } + if (!url) { url = await vscode.window.showInputBox({ placeHolder: vscode.l10n.t("https://example.com"), @@ -55,11 +82,15 @@ export function activate(context: vscode.ExtensionContext) { } })); - context.subscriptions.push(vscode.commands.registerCommand(openApiCommand, (url: vscode.Uri, showOptions?: { + context.subscriptions.push(vscode.commands.registerCommand(openApiCommand, async (url: vscode.Uri, showOptions?: { preserveFocus?: boolean; viewColumn: vscode.ViewColumn; }) => { - manager.show(url, showOptions); + if (await shouldUseIntegratedBrowser()) { + await openInIntegratedBrowser(url.toString(true)); + } else { + manager.show(url, showOptions); + } })); context.subscriptions.push(vscode.window.registerExternalUriOpener(openerId, { @@ -74,10 +105,14 @@ export function activate(context: vscode.ExtensionContext) { return vscode.ExternalUriOpenerPriority.None; }, - openExternalUri(resolveUri: vscode.Uri) { - return manager.show(resolveUri, { - viewColumn: vscode.window.activeTextEditor ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active - }); + async openExternalUri(resolveUri: vscode.Uri) { + if (await shouldUseIntegratedBrowser()) { + await openInIntegratedBrowser(resolveUri.toString(true)); + } else { + return manager.show(resolveUri, { + viewColumn: vscode.window.activeTextEditor ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active + }); + } } }, { schemes: ['http', 'https'], diff --git a/extensions/theme-defaults/themes/dark_modern.json b/extensions/theme-defaults/themes/dark_modern.json index 51e0f371c27cd..574d89f9c4a66 100644 --- a/extensions/theme-defaults/themes/dark_modern.json +++ b/extensions/theme-defaults/themes/dark_modern.json @@ -13,12 +13,12 @@ "badge.background": "#616161", "badge.foreground": "#F8F8F8", "button.background": "#0078D4", - "button.border": "#FFFFFF12", + "button.border": "#ffffff1a", "button.foreground": "#FFFFFF", "button.hoverBackground": "#026EC1", - "button.secondaryBackground": "#313131", + "button.secondaryBackground": "#00000000", "button.secondaryForeground": "#CCCCCC", - "button.secondaryHoverBackground": "#3C3C3C", + "button.secondaryHoverBackground": "#2B2B2B", "chat.slashCommandBackground": "#26477866", "chat.slashCommandForeground": "#85B6FF", "chat.editedFileForeground": "#E2C08D", diff --git a/extensions/tunnel-forwarding/src/extension.ts b/extensions/tunnel-forwarding/src/extension.ts index 299c728719f78..4752167e6f2e8 100644 --- a/extensions/tunnel-forwarding/src/extension.ts +++ b/extensions/tunnel-forwarding/src/extension.ts @@ -25,7 +25,11 @@ const cliPath = process.env.VSCODE_FORWARDING_IS_DEV ? path.join(__dirname, '../../../cli/target/debug/code') : path.join( vscode.env.appRoot, - process.platform === 'darwin' ? 'bin' : '../../bin', + process.platform === 'darwin' + ? 'bin' + : process.platform === 'win32' && vscode.env.appQuality === 'insider' + ? '../../../bin' // TODO: remove as part of https://github.com/microsoft/vscode/issues/282514 + : '../../bin', vscode.env.appQuality === 'stable' ? 'code-tunnel' : 'code-tunnel-insiders', ) + (process.platform === 'win32' ? '.exe' : ''); diff --git a/package-lock.json b/package-lock.json index fc9b6b0f7a6ec..b14a6d39f8e28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "native-is-elevated": "0.8.0", "native-keymap": "^3.3.5", "native-watchdog": "^1.4.1", - "node-pty": "^1.1.0-beta43", + "node-pty": "^1.2.0-beta.6", "open": "^10.1.2", "tas-client": "0.3.1", "undici": "^7.9.0", @@ -12950,9 +12950,9 @@ } }, "node_modules/node-pty": { - "version": "1.1.0-beta43", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta43.tgz", - "integrity": "sha512-CYyIQogRs97Rfjo0WKyku8V56Bm4WyWUijrbWDs5LJ+ZmsUW2gqbVAEpD+1gtA7dEZ6v1A08GzfqsDuIl/eRqw==", + "version": "1.2.0-beta.6", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.6.tgz", + "integrity": "sha512-0ArHUpsE5y6nSRSkbY36l+bjyuZNMjww0pdsBKCbiw/HTFCikJlsbUuyZc60KPdgH/9YhAiqD2BM8a0AOUVrsw==", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e5ad7191f8756..f698bd3808f68 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.109.0", - "distro": "ce89ce05183635114ccfc46870d71ec520727c8e", + "distro": "b570759d1928b4b2f34a86c40da42b1a2b6d3796", "author": { "name": "Microsoft Corporation" }, @@ -109,7 +109,7 @@ "native-is-elevated": "0.8.0", "native-keymap": "^3.3.5", "native-watchdog": "^1.4.1", - "node-pty": "^1.1.0-beta43", + "node-pty": "^1.2.0-beta.6", "open": "^10.1.2", "tas-client": "0.3.1", "undici": "^7.9.0", @@ -240,4 +240,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} +} \ No newline at end of file diff --git a/remote/package-lock.json b/remote/package-lock.json index 30c5541fd6098..fd2b8a14beeaf 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -38,7 +38,7 @@ "kerberos": "2.1.1", "minimist": "^1.2.8", "native-watchdog": "^1.4.1", - "node-pty": "^1.1.0-beta43", + "node-pty": "^1.2.0-beta.6", "tas-client": "0.3.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", @@ -1052,9 +1052,9 @@ } }, "node_modules/node-pty": { - "version": "1.1.0-beta43", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta43.tgz", - "integrity": "sha512-CYyIQogRs97Rfjo0WKyku8V56Bm4WyWUijrbWDs5LJ+ZmsUW2gqbVAEpD+1gtA7dEZ6v1A08GzfqsDuIl/eRqw==", + "version": "1.2.0-beta.6", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.6.tgz", + "integrity": "sha512-0ArHUpsE5y6nSRSkbY36l+bjyuZNMjww0pdsBKCbiw/HTFCikJlsbUuyZc60KPdgH/9YhAiqD2BM8a0AOUVrsw==", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/remote/package.json b/remote/package.json index d2eab8bf24a21..f506788e938e2 100644 --- a/remote/package.json +++ b/remote/package.json @@ -33,7 +33,7 @@ "kerberos": "2.1.1", "minimist": "^1.2.8", "native-watchdog": "^1.4.1", - "node-pty": "^1.1.0-beta43", + "node-pty": "^1.2.0-beta.6", "tas-client": "0.3.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", diff --git a/src/vs/base/browser/ui/button/button.css b/src/vs/base/browser/ui/button/button.css index 2517cd3571ca9..b641c7fc50c3c 100644 --- a/src/vs/base/browser/ui/button/button.css +++ b/src/vs/base/browser/ui/button/button.css @@ -7,14 +7,21 @@ box-sizing: border-box; display: flex; width: 100%; - padding: 4px; - border-radius: 2px; + padding: 4px 8px; + border-radius: 4px; text-align: center; cursor: pointer; justify-content: center; align-items: center; border: 1px solid var(--vscode-button-border, transparent); - line-height: 18px; + line-height: 16px; + font-size: 12px; +} + +.monaco-text-button.small { + line-height: 14px; + font-size: 11px; + padding: 3px 6px; } .monaco-text-button:focus { @@ -61,6 +68,7 @@ align-items: center; font-weight: normal; font-style: inherit; + line-height: 18px; padding: 4px 0; } @@ -100,13 +108,13 @@ .monaco-button-dropdown > .monaco-button.monaco-dropdown-button { border: 1px solid var(--vscode-button-border, transparent); border-left-width: 0 !important; - border-radius: 0 2px 2px 0; + border-radius: 0 4px 4px 0; display: flex; align-items: center; } .monaco-button-dropdown > .monaco-button.monaco-text-button { - border-radius: 2px 0 0 2px; + border-radius: 4px 0 0 4px; } .monaco-description-button { diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 9b66a126cb9b0..fa1fa93d5451b 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -35,6 +35,7 @@ export interface IButtonOptions extends Partial { readonly supportIcons?: boolean; readonly supportShortLabel?: boolean; readonly secondary?: boolean; + readonly small?: boolean; readonly hoverDelegate?: IHoverDelegate; readonly disabled?: boolean; } @@ -116,6 +117,7 @@ export class Button extends Disposable implements IButton { this._element.setAttribute('role', 'button'); this._element.classList.toggle('secondary', !!options.secondary); + this._element.classList.toggle('small', !!options.small); const background = options.secondary ? options.buttonSecondaryBackground : options.buttonBackground; const foreground = options.secondary ? options.buttonSecondaryForeground : options.buttonForeground; diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index fe18c9a447b7c..c484fa86dbd94 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -194,7 +194,6 @@ } .monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button { - padding: 4px 10px; overflow: hidden; text-overflow: ellipsis; margin: 4px 5px; /* allows button focus outline to be visible */ @@ -228,19 +227,14 @@ outline-width: 1px; outline-style: solid; outline-color: var(--vscode-focusBorder); - border-radius: 2px; + border-radius: 4px; } -.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button-dropdown > .monaco-text-button { - padding-left: 10px; - padding-right: 10px; -} .monaco-dialog-box.align-vertical > .dialog-buttons-row > .dialog-buttons > .monaco-button-dropdown > .monaco-text-button { width: 100%; } .monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button-dropdown > .monaco-dropdown-button { - padding-left: 5px; - padding-right: 5px; + padding: 0 4px; } diff --git a/src/vs/base/browser/ui/inputbox/inputBox.css b/src/vs/base/browser/ui/inputbox/inputBox.css index f6005a48f7894..827a19f29b487 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.css +++ b/src/vs/base/browser/ui/inputbox/inputBox.css @@ -8,7 +8,7 @@ display: block; padding: 0; box-sizing: border-box; - border-radius: 2px; + border-radius: 4px; /* Customizable */ font-size: inherit; diff --git a/src/vs/base/browser/ui/selectBox/selectBox.css b/src/vs/base/browser/ui/selectBox/selectBox.css index 7242251e9b460..2b0011a842bfe 100644 --- a/src/vs/base/browser/ui/selectBox/selectBox.css +++ b/src/vs/base/browser/ui/selectBox/selectBox.css @@ -6,7 +6,7 @@ .monaco-select-box { width: 100%; cursor: pointer; - border-radius: 2px; + border-radius: 4px; } .monaco-select-box-dropdown-container { @@ -30,6 +30,6 @@ .mac .monaco-action-bar .action-item .monaco-select-box { font-size: 11px; - border-radius: 3px; + border-radius: 4px; min-height: 24px; } diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.css b/src/vs/base/browser/ui/selectBox/selectBoxCustom.css index 4d2fb516f2029..2ca9a99a7bc6e 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.css +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.css @@ -6,7 +6,7 @@ .monaco-select-box-dropdown-container { display: none; box-sizing: border-box; - border-radius: 5px; + border-radius: 4px; box-shadow: 0 2px 8px var(--vscode-widget-shadow); } diff --git a/src/vs/editor/contrib/rename/browser/renameWidget.css b/src/vs/editor/contrib/rename/browser/renameWidget.css index 66f241efd1c73..acd375f2afb7e 100644 --- a/src/vs/editor/contrib/rename/browser/renameWidget.css +++ b/src/vs/editor/contrib/rename/browser/renameWidget.css @@ -15,7 +15,7 @@ .monaco-editor .rename-box .rename-input-with-button { padding: 3px; - border-radius: 2px; + border-radius: 4px; width: calc(100% - 8px); /* 4px padding on each side */ } diff --git a/src/vs/platform/accessibility/browser/accessibleView.ts b/src/vs/platform/accessibility/browser/accessibleView.ts index adcf1f0e5e2e7..764c4ff0a6ca9 100644 --- a/src/vs/platform/accessibility/browser/accessibleView.ts +++ b/src/vs/platform/accessibility/browser/accessibleView.ts @@ -21,6 +21,7 @@ export const enum AccessibleViewProviderId { MergeEditor = 'mergeEditor', PanelChat = 'panelChat', ChatTerminalOutput = 'chatTerminalOutput', + ChatThinking = 'chatThinking', InlineChat = 'inlineChat', AgentChat = 'agentChat', QuickChat = 'quickChat', diff --git a/src/vs/platform/actions/browser/buttonbar.ts b/src/vs/platform/actions/browser/buttonbar.ts index 45778e15a54d6..f6488250bbafa 100644 --- a/src/vs/platform/actions/browser/buttonbar.ts +++ b/src/vs/platform/actions/browser/buttonbar.ts @@ -29,6 +29,7 @@ export type IButtonConfigProvider = (action: IAction, index: number) => { export interface IWorkbenchButtonBarOptions { telemetrySource?: string; buttonConfigProvider?: IButtonConfigProvider; + small?: boolean; } export class WorkbenchButtonBar extends ButtonBar { @@ -99,6 +100,7 @@ export class WorkbenchButtonBar extends ButtonBar { contextMenuProvider: this._contextMenuService, ariaLabel: tooltip, supportIcons: true, + small: this._options?.small, }); } else { action = actionOrSubmenu; @@ -106,6 +108,7 @@ export class WorkbenchButtonBar extends ButtonBar { secondary: conifgProvider(action, i)?.isSecondary ?? secondary, ariaLabel: tooltip, supportIcons: true, + small: this._options?.small, }); } @@ -142,7 +145,8 @@ export class WorkbenchButtonBar extends ButtonBar { const btn = this.addButton({ secondary: true, - ariaLabel: localize('moreActions', "More Actions") + ariaLabel: localize('moreActions', "More Actions"), + small: this._options?.small, }); btn.icon = Codicon.dropDownButton; diff --git a/src/vs/platform/browserView/electron-main/browserView.ts b/src/vs/platform/browserView/electron-main/browserView.ts index 41d1c8e9522ba..a6e0856069e3e 100644 --- a/src/vs/platform/browserView/electron-main/browserView.ts +++ b/src/vs/platform/browserView/electron-main/browserView.ts @@ -8,7 +8,7 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { VSBuffer } from '../../../base/common/buffer.js'; import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions } from '../common/browserView.js'; -import { EVENT_KEY_CODE_MAP, KeyCode, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js'; +import { EVENT_KEY_CODE_MAP, KeyCode, KeyMod, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js'; import { IThemeMainService } from '../../theme/electron-main/themeMainService.js'; import { IWindowsMainService } from '../../windows/electron-main/windows.js'; import { IBaseWindow, ICodeWindow } from '../../window/electron-main/window.js'; @@ -17,19 +17,17 @@ import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryW import { ILogService } from '../../log/common/log.js'; import { isMacintosh } from '../../../base/common/platform.js'; -const nativeShortcutKeys = new Set(['KeyA', 'KeyC', 'KeyV', 'KeyX', 'KeyZ']); -function shouldIgnoreNativeShortcut(input: Electron.Input): boolean { - const isControlInput = isMacintosh ? input.meta : input.control; - const isAltOnlyInput = input.alt && !input.control && !input.meta; - - // Ignore Alt-only inputs (often used for accented characters or menu accelerators) - if (isAltOnlyInput) { - return true; - } - - // Ignore Ctrl/Cmd + [A,C,V,X,Z] shortcuts to allow native handling (e.g. copy/paste) - return isControlInput && nativeShortcutKeys.has(input.code); -} +/** Key combinations that are used in system-level shortcuts. */ +const nativeShortcuts = new Set([ + KeyMod.CtrlCmd | KeyCode.KeyA, + KeyMod.CtrlCmd | KeyCode.KeyC, + KeyMod.CtrlCmd | KeyCode.KeyV, + KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyV, + KeyMod.CtrlCmd | KeyCode.KeyX, + ...(isMacintosh ? [] : [KeyMod.CtrlCmd | KeyCode.KeyY]), + KeyMod.CtrlCmd | KeyCode.KeyZ, + KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ +]); /** * Represents a single browser view instance with its WebContentsView and all associated logic. @@ -237,28 +235,8 @@ export class BrowserView extends Disposable { // Key down events - listen for raw key input events webContents.on('before-input-event', async (event, input) => { if (input.type === 'keyDown' && !this._isSendingKeyEvent) { - if (shouldIgnoreNativeShortcut(input)) { - return; - } - const eventKeyCode = SCAN_CODE_STR_TO_EVENT_KEY_CODE[input.code] || 0; - const keyCode = EVENT_KEY_CODE_MAP[eventKeyCode] || KeyCode.Unknown; - const hasCommandModifier = input.control || input.alt || input.meta; - const isNonEditingKey = - keyCode >= KeyCode.F1 && keyCode <= KeyCode.F24 || - keyCode >= KeyCode.AudioVolumeMute; - - if (hasCommandModifier || isNonEditingKey) { + if (this.tryHandleCommand(input)) { event.preventDefault(); - this._onDidKeyCommand.fire({ - key: input.key, - keyCode: eventKeyCode, - code: input.code, - ctrlKey: input.control || false, - shiftKey: input.shift || false, - altKey: input.alt || false, - metaKey: input.meta || false, - repeat: input.isAutoRepeat || false - }); } } }); @@ -467,6 +445,54 @@ export class BrowserView extends Disposable { super.dispose(); } + /** + * Potentially handle an input event as a VS Code command. + * Returns `true` if the event was forwarded to VS Code and should not be handled natively. + */ + private tryHandleCommand(input: Electron.Input): boolean { + const eventKeyCode = SCAN_CODE_STR_TO_EVENT_KEY_CODE[input.code] || 0; + const keyCode = EVENT_KEY_CODE_MAP[eventKeyCode] || KeyCode.Unknown; + + const isArrowKey = keyCode >= KeyCode.LeftArrow && keyCode <= KeyCode.DownArrow; + const isNonEditingKey = + keyCode === KeyCode.Escape || + keyCode >= KeyCode.F1 && keyCode <= KeyCode.F24 || + keyCode >= KeyCode.AudioVolumeMute; + + // Ignore most Alt-only inputs (often used for accented characters or menu accelerators) + const isAltOnlyInput = input.alt && !input.control && !input.meta; + if (isAltOnlyInput && !isNonEditingKey && !isArrowKey) { + return false; + } + + // Only reroute if there's a command modifier or it's a non-editing key + const hasCommandModifier = input.control || input.alt || input.meta; + if (!hasCommandModifier && !isNonEditingKey) { + return false; + } + + // Ignore Ctrl/Cmd + [A,C,V,X,Z] shortcuts to allow native handling (e.g. copy/paste) + const isControlInput = isMacintosh ? input.meta : input.control; + const modifiedKeyCode = keyCode | + (isControlInput ? KeyMod.CtrlCmd : 0) | + (input.shift ? KeyMod.Shift : 0) | + (input.alt ? KeyMod.Alt : 0); + if (nativeShortcuts.has(modifiedKeyCode)) { + return false; + } + + this._onDidKeyCommand.fire({ + key: input.key, + keyCode: eventKeyCode, + code: input.code, + ctrlKey: input.control || false, + shiftKey: input.shift || false, + altKey: input.alt || false, + metaKey: input.meta || false, + repeat: input.isAutoRepeat || false + }); + return true; + } private windowById(windowId: number | undefined): ICodeWindow | IAuxiliaryWindow | undefined { return this.codeWindowById(windowId) ?? this.auxiliaryWindowById(windowId); diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 2a0fdff9f2430..31fab40ccf3ff 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -182,6 +182,9 @@ const _allApiProposals = { contribViewsWelcome: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts', }, + css: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.css.d.ts', + }, customEditorMove: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts', }, diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 0b0856c6411e2..0636687742da0 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -9,7 +9,7 @@ z-index: 2550; left: 50%; -webkit-app-region: no-drag; - border-radius: 6px; + border-radius: 8px; } .quick-input-titlebar { @@ -89,7 +89,7 @@ .quick-input-header { cursor: grab; display: flex; - padding: 6px 6px 2px 6px; + padding: 6px 6px 4px 6px; } .quick-input-widget.hidden-input .quick-input-header { @@ -155,14 +155,6 @@ margin-left: 6px; } -.quick-input-action .monaco-text-button { - font-size: 11px; - padding: 0 6px; - display: flex; - height: 25px; - align-items: center; -} - .quick-input-message { margin-top: -1px; padding: 5px; @@ -196,7 +188,7 @@ .quick-input-list .monaco-list { overflow: hidden; max-height: calc(20 * 22px); - padding-bottom: 5px; + padding-bottom: 7px; } .quick-input-list .monaco-scrollable-element { diff --git a/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts b/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts index 328f5f941ef82..a9a3f6a90d71e 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts @@ -113,6 +113,7 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { onCommandStart: Event, onCommandStartChanged: Event, onCommandExecuted: Event, + onCommandFinished: Event, @ILogService private readonly _logService: ILogService ) { super(); @@ -127,6 +128,7 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { this._register(onCommandStart(e => this._handleCommandStart(e as { marker: IMarker }))); this._register(onCommandStartChanged(() => this._handleCommandStartChanged())); this._register(onCommandExecuted(() => this._handleCommandExecuted())); + this._register(onCommandFinished(() => this._handleCommandFinished())); this._register(this.onDidStartInput(() => this._logCombinedStringIfTrace('PromptInputModel#onDidStartInput'))); this._register(this.onDidChangeInput(() => this._logCombinedStringIfTrace('PromptInputModel#onDidChangeInput'))); @@ -261,6 +263,13 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { this._onDidChangeInput.fire(event); } + private _handleCommandFinished() { + // Clear the prompt input value when command finishes to prepare for the next command + // This prevents runCommand from detecting leftover text and sending ^C unnecessarily + this._value = ''; + this._onDidChangeInput.fire(this._createStateObject()); + } + @throttle(0) private _sync() { try { diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index e52d286d20e9b..259fc6ef00c85 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -84,7 +84,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe ) { super(); this._currentCommand = new PartialTerminalCommand(this._terminal); - this._promptInputModel = this._register(new PromptInputModel(this._terminal, this.onCommandStarted, this.onCommandStartChanged, this.onCommandExecuted, this._logService)); + this._promptInputModel = this._register(new PromptInputModel(this._terminal, this.onCommandStarted, this.onCommandStartChanged, this.onCommandExecuted, this.onCommandFinished, this._logService)); // Pull command line from the buffer if it was not set explicitly this._register(this.onCommandExecuted(command => { diff --git a/src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts b/src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts index 64fe94b7ab953..e625ae66a9b86 100644 --- a/src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts +++ b/src/vs/platform/terminal/test/common/capabilities/commandDetection/promptInputModel.test.ts @@ -24,6 +24,7 @@ suite('PromptInputModel', () => { let onCommandStart: Emitter; let onCommandStartChanged: Emitter; let onCommandExecuted: Emitter; + let onCommandFinished: Emitter; async function writePromise(data: string) { await new Promise(r => xterm.write(data, r)); @@ -37,6 +38,10 @@ suite('PromptInputModel', () => { onCommandExecuted.fire(null!); } + function fireCommandFinished() { + onCommandFinished.fire(null!); + } + function setContinuationPrompt(prompt: string) { promptInputModel.setContinuationPrompt(prompt); } @@ -68,7 +73,8 @@ suite('PromptInputModel', () => { onCommandStart = store.add(new Emitter()); onCommandStartChanged = store.add(new Emitter()); onCommandExecuted = store.add(new Emitter()); - promptInputModel = store.add(new PromptInputModel(xterm, onCommandStart.event, onCommandStartChanged.event, onCommandExecuted.event, new NullLogService)); + onCommandFinished = store.add(new Emitter()); + promptInputModel = store.add(new PromptInputModel(xterm, onCommandStart.event, onCommandStartChanged.event, onCommandExecuted.event, onCommandFinished.event, new NullLogService)); }); test('basic input and execute', async () => { @@ -138,6 +144,21 @@ suite('PromptInputModel', () => { }); }); + test('should clear value when command finishes', async () => { + await writePromise('$ '); + fireCommandStart(); + await assertPromptInput('|'); + + await writePromise('echo hello'); + await assertPromptInput('echo hello|'); + + fireCommandExecuted(); + strictEqual(promptInputModel.value, 'echo hello'); + + fireCommandFinished(); + strictEqual(promptInputModel.value, ''); + }); + test('cursor navigation', async () => { await writePromise('$ '); fireCommandStart(); diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 31f148f202475..bfb284d95117f 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -13,6 +13,7 @@ import { IconExtensionPoint } from '../../services/themes/common/iconExtensionPo import { TokenClassificationExtensionPoints } from '../../services/themes/common/tokenClassificationExtensionPoint.js'; import { LanguageConfigurationFileHandler } from '../../contrib/codeEditor/common/languageConfigurationExtensionPoint.js'; import { StatusBarItemsExtensionPoint } from './statusBarExtensionPoint.js'; +import { CSSExtensionPoint } from '../../services/themes/browser/cssExtensionPoint.js'; // --- mainThread participants import './mainThreadLocalization.js'; @@ -110,6 +111,7 @@ export class ExtensionPoints implements IWorkbenchContribution { this.instantiationService.createInstance(TokenClassificationExtensionPoints); this.instantiationService.createInstance(LanguageConfigurationFileHandler); this.instantiationService.createInstance(StatusBarItemsExtensionPoint); + this.instantiationService.createInstance(CSSExtensionPoint); } } diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 6444ca9c12c12..0d56d40bb743a 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -35,6 +35,7 @@ import { ChatRequestParser } from '../../contrib/chat/common/requestParser/chatR import { IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatNotebookEdit, IChatProgress, IChatService, IChatTask, IChatTaskSerialized, IChatWarningMessage } from '../../contrib/chat/common/chatService/chatService.js'; import { IChatSessionsService } from '../../contrib/chat/common/chatSessionsService.js'; import { ChatAgentLocation, ChatModeKind } from '../../contrib/chat/common/constants.js'; +import { ILanguageModelToolsService } from '../../contrib/chat/common/tools/languageModelToolsService.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { IExtensionService } from '../../services/extensions/common/extensions.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; @@ -120,6 +121,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA @IExtensionService private readonly _extensionService: IExtensionService, @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService, @IPromptsService private readonly _promptsService: IPromptsService, + @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2); @@ -279,6 +281,23 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA continue; } + if (progress.kind === 'beginToolInvocation') { + // Begin a streaming tool invocation + this._languageModelToolsService.beginToolCall({ + toolCallId: progress.toolCallId, + toolId: progress.toolName, + chatRequestId: requestId, + sessionResource: chatSession?.sessionResource, + }); + continue; + } + + if (progress.kind === 'updateToolInvocation') { + // Update the streaming data for an existing tool invocation + this._languageModelToolsService.updateToolStream(progress.toolCallId, progress.streamData?.partialInput, CancellationToken.None); + continue; + } + const revivedProgress = progress.kind === 'notebookEdit' ? ChatNotebookEdit.fromChatEdit(progress) : revive(progress) as IChatProgress; diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts index 00336ecfc7187..a0686773ff40b 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts @@ -93,6 +93,7 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre } }, prepareToolInvocation: (context, token) => this._proxy.$prepareToolInvocation(id, context, token), + handleToolStream: (context, token) => this._proxy.$handleToolStream(id, context, token), }); this._tools.set(id, disposable); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index a77a0079ee02e..ee8bf713db5d1 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1906,7 +1906,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseExtensionsPart: extHostTypes.ChatResponseExtensionsPart, ChatResponseExternalEditPart: extHostTypes.ChatResponseExternalEditPart, ChatResponsePullRequestPart: extHostTypes.ChatResponsePullRequestPart, - ChatPrepareToolInvocationPart: extHostTypes.ChatPrepareToolInvocationPart, ChatResponseMultiDiffPart: extHostTypes.ChatResponseMultiDiffPart, ChatResponseReferencePartStatusKind: extHostTypes.ChatResponseReferencePartStatusKind, ChatResponseClearToPreviousToolInvocationReason: extHostTypes.ChatResponseClearToPreviousToolInvocationReason, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 000764668baab..c6c821007c47b 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -64,7 +64,7 @@ import { IChatSessionItem, IChatSessionProviderOptionGroup, IChatSessionProvider import { IChatRequestVariableValue } from '../../contrib/chat/common/attachments/chatVariables.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatMessage, IChatResponsePart, ILanguageModelChatInfoOptions, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatSelector } from '../../contrib/chat/common/languageModels.js'; -import { IPreparedToolInvocation, IToolInvocation, IToolInvocationPreparationContext, IToolProgressStep, IToolResult, ToolDataSource } from '../../contrib/chat/common/tools/languageModelToolsService.js'; +import { IPreparedToolInvocation, IStreamedToolInvocation, IToolInvocation, IToolInvocationPreparationContext, IToolInvocationStreamContext, IToolProgressStep, IToolResult, ToolDataSource } from '../../contrib/chat/common/tools/languageModelToolsService.js'; import { IPromptFileContext, IPromptFileResource } from '../../contrib/chat/common/promptSyntax/service/promptsService.js'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js'; import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from '../../contrib/mcp/common/mcpTypes.js'; @@ -1509,6 +1509,7 @@ export interface ExtHostLanguageModelToolsShape { $invokeTool(dto: Dto, token: CancellationToken): Promise | SerializableObjectWithBuffers>>; $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise; + $handleToolStream(toolId: string, context: IToolInvocationStreamContext, token: CancellationToken): Promise; $prepareToolInvocation(toolId: string, context: IToolInvocationPreparationContext, token: CancellationToken): Promise; } @@ -1535,7 +1536,9 @@ export type IChatProgressDto = | IChatTaskDto | IChatNotebookEditDto | IChatExternalEditsDto - | IChatResponseClearToPreviousToolInvocationDto; + | IChatResponseClearToPreviousToolInvocationDto + | IChatBeginToolInvocationDto + | IChatUpdateToolInvocationDto; export interface ExtHostUrlsShape { $handleExternalUri(handle: number, uri: UriComponents): Promise; @@ -2320,6 +2323,23 @@ export interface IChatResponseClearToPreviousToolInvocationDto { reason: ChatResponseClearToPreviousToolInvocationReason; } +export interface IChatBeginToolInvocationDto { + kind: 'beginToolInvocation'; + toolCallId: string; + toolName: string; + streamData?: { + partialInput?: unknown; + }; +} + +export interface IChatUpdateToolInvocationDto { + kind: 'updateToolInvocation'; + toolCallId: string; + streamData: { + partialInput?: unknown; + }; +} + export type ICellEditOperationDto = notebookCommon.ICellMetadataEdit | notebookCommon.IDocumentMetadataEdit diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 493723aa848ed..1f47b51ee412d 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -112,7 +112,7 @@ export class ChatAgentResponseStream { const _report = (progress: IChatProgressDto, task?: (progress: vscode.Progress) => Thenable) => { // Measure the time to the first progress update with real markdown content - if (typeof this._firstProgress === 'undefined' && (progress.kind === 'markdownContent' || progress.kind === 'markdownVuln' || progress.kind === 'prepareToolInvocation')) { + if (typeof this._firstProgress === 'undefined' && (progress.kind === 'markdownContent' || progress.kind === 'markdownVuln' || progress.kind === 'beginToolInvocation')) { this._firstProgress = this._stopWatch.elapsed(); } @@ -301,12 +301,32 @@ export class ChatAgentResponseStream { _report(dto); return this; }, - prepareToolInvocation(toolName) { - throwIfDone(this.prepareToolInvocation); + beginToolInvocation(toolCallId, toolName, streamData) { + throwIfDone(this.beginToolInvocation); checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); - const part = new extHostTypes.ChatPrepareToolInvocationPart(toolName); - const dto = typeConvert.ChatPrepareToolInvocationPart.from(part); + const dto: IChatProgressDto = { + kind: 'beginToolInvocation', + toolCallId, + toolName, + streamData: streamData ? { + partialInput: streamData.partialInput + } : undefined + }; + _report(dto); + return this; + }, + updateToolInvocation(toolCallId, streamData) { + throwIfDone(this.updateToolInvocation); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + + const dto: IChatProgressDto = { + kind: 'updateToolInvocation', + toolCallId, + streamData: { + partialInput: streamData.partialInput + } + }; _report(dto); return this; }, @@ -357,11 +377,6 @@ export class ChatAgentResponseStream { that._sessionDisposables.add(toDisposable(() => cts.dispose(true))); } _report(dto); - } else if (part instanceof extHostTypes.ChatPrepareToolInvocationPart) { - checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); - const dto = typeConvert.ChatPrepareToolInvocationPart.from(part); - _report(dto); - return this; } else if (part instanceof extHostTypes.ChatResponseExternalEditPart) { const p = this.externalEdit(part.uris, part.callback); p.then((value) => part.didGetApplied(value)); diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index ab4eb8822ef68..f629148a38973 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -12,7 +12,7 @@ import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; import { revive } from '../../../base/common/marshalling.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; -import { IPreparedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolInvocationPreparationContext, IToolResult, ToolInvocationPresentation } from '../../contrib/chat/common/tools/languageModelToolsService.js'; +import { IPreparedToolInvocation, IStreamedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolInvocationPreparationContext, IToolInvocationStreamContext, IToolResult, ToolInvocationPresentation } from '../../contrib/chat/common/tools/languageModelToolsService.js'; import { ExtensionEditToolId, InternalEditToolId } from '../../contrib/chat/common/tools/builtinTools/editFileTool.js'; import { InternalFetchWebPageToolId } from '../../contrib/chat/common/tools/builtinTools/tools.js'; import { SearchExtensionsToolId } from '../../contrib/extensions/common/searchExtensionsTool.js'; @@ -126,6 +126,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape chatRequestId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatRequestId : undefined, chatInteractionId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatInteractionId : undefined, fromSubAgent: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.fromSubAgent : undefined, + chatStreamToolCallId: isProposedApiEnabled(extension, 'chatParticipantAdditions') ? options.chatStreamToolCallId : undefined, }, token); const dto: Dto = result instanceof SerializableObjectWithBuffers ? result.value : result; @@ -191,6 +192,9 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape if (isProposedApiEnabled(item.extension, 'chatParticipantAdditions') && dto.modelId) { options.model = await this.getModel(dto.modelId, item.extension); } + if (isProposedApiEnabled(item.extension, 'chatParticipantAdditions') && dto.chatStreamToolCallId) { + options.chatStreamToolCallId = dto.chatStreamToolCallId; + } if (dto.tokenBudget !== undefined) { options.tokenizationOptions = { @@ -242,6 +246,37 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape return model; } + async $handleToolStream(toolId: string, context: IToolInvocationStreamContext, token: CancellationToken): Promise { + const item = this._registeredTools.get(toolId); + if (!item) { + throw new Error(`Unknown tool ${toolId}`); + } + + // Only call handleToolStream if it's defined on the tool + if (!item.tool.handleToolStream) { + return undefined; + } + + // Ensure the chatParticipantAdditions API is enabled + checkProposedApiEnabled(item.extension, 'chatParticipantAdditions'); + + const options: vscode.LanguageModelToolInvocationStreamOptions = { + rawInput: context.rawInput, + chatRequestId: context.chatRequestId, + chatSessionId: context.chatSessionId, + chatInteractionId: context.chatInteractionId + }; + + const result = await item.tool.handleToolStream(options, token); + if (!result) { + return undefined; + } + + return { + invocationMessage: typeConvert.MarkdownString.fromStrict(result.invocationMessage) + }; + } + async $prepareToolInvocation(toolId: string, context: IToolInvocationPreparationContext, token: CancellationToken): Promise { const item = this._registeredTools.get(toolId); if (!item) { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 592ca855aa6d2..d12a5b9c3756c 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -42,7 +42,7 @@ import { IViewBadge } from '../../common/views.js'; import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/participants/chatAgents.js'; import { IChatRequestDraft } from '../../contrib/chat/common/editing/chatEditingService.js'; import { IChatRequestModeInstructions } from '../../contrib/chat/common/model/chatModel.js'; -import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatMultiDiffDataSerialized, IChatPrepareToolInvocationPart, IChatProgressMessage, IChatPullRequestContent, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatThinkingPart, IChatToolInvocationSerialized, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService/chatService.js'; +import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatMultiDiffDataSerialized, IChatProgressMessage, IChatPullRequestContent, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatThinkingPart, IChatToolInvocationSerialized, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService/chatService.js'; import { LocalChatSessionUri } from '../../contrib/chat/common/model/chatUri.js'; import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isImageVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; @@ -2813,19 +2813,6 @@ export namespace ChatResponseMovePart { } } -export namespace ChatPrepareToolInvocationPart { - export function from(part: vscode.ChatPrepareToolInvocationPart): IChatPrepareToolInvocationPart { - return { - kind: 'prepareToolInvocation', - toolName: part.toolName, - }; - } - - export function to(part: IChatPrepareToolInvocationPart): vscode.ChatPrepareToolInvocationPart { - return new types.ChatPrepareToolInvocationPart(part.toolName); - } -} - export namespace ChatToolInvocationPart { export function from(part: vscode.ChatToolInvocationPart): IChatToolInvocationSerialized { // Convert extension API ChatToolInvocationPart to internal serialized format @@ -3098,8 +3085,6 @@ export namespace ChatResponsePart { return ChatResponseMovePart.from(part); } else if (part instanceof types.ChatResponseExtensionsPart) { return ChatResponseExtensionsPart.from(part); - } else if (part instanceof types.ChatPrepareToolInvocationPart) { - return ChatPrepareToolInvocationPart.from(part); } else if (part instanceof types.ChatResponsePullRequestPart) { return ChatResponsePullRequestPart.from(part); } else if (part instanceof types.ChatToolInvocationPart) { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 41cbfdd173858..6277175ffcd98 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3340,17 +3340,6 @@ export class ChatResponseNotebookEditPart implements vscode.ChatResponseNotebook } } -export class ChatPrepareToolInvocationPart { - toolName: string; - /** - * @param toolName The name of the tool being prepared for invocation. - */ - constructor(toolName: string) { - this.toolName = toolName; - } -} - - export interface ChatTerminalToolInvocationData2 { commandLine: { original: string; diff --git a/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css b/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css index 4861d18435350..b7c1b96fc9a59 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css +++ b/src/vs/workbench/browser/parts/editor/media/editorplaceholder.css @@ -57,8 +57,6 @@ } .monaco-editor-pane-placeholder .editor-placeholder-buttons-container > .monaco-button { - font-size: 14px; width: fit-content; - padding: 6px 11px; outline-offset: 2px !important; } diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsList.css b/src/vs/workbench/browser/parts/notifications/media/notificationsList.css index e41d6f4824aa4..92da46b4dca81 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsList.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsList.css @@ -127,9 +127,7 @@ .monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-buttons-container .monaco-text-button { width: fit-content; - padding: 4px 10px; display: inline-block; /* to enable ellipsis in text overflow */ - font-size: 12px; overflow: hidden; text-overflow: ellipsis; } diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 0246cd2ad108c..982f5a620df98 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -168,10 +168,7 @@ border: 1px solid var(--vscode-commandCenter-border); overflow: hidden; margin: 0 6px; - border-top-left-radius: 6px; - border-bottom-left-radius: 6px; - border-top-right-radius: 6px; - border-bottom-right-radius: 6px; + border-radius: 4px; height: 22px; width: 38vw; max-width: 600px; diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index 554cc6279ada3..064dc540ab29f 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -6,6 +6,7 @@ import './media/browser.css'; import { localize } from '../../../../nls.js'; import { $, addDisposableListener, disposableWindowInterval, EventType, scheduleAtNextAnimationFrame } from '../../../../base/browser/dom.js'; +import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { RawContextKey, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; @@ -101,7 +102,7 @@ class BrowserNavigationBar extends Disposable { { hoverDelegate, highlightToggledItems: true, - toolbarOptions: { primaryGroup: 'actions' }, + toolbarOptions: { primaryGroup: (group) => group.startsWith('actions'), useSeparatorsInPrimaryActions: true }, menuOptions: { shouldForwardArgs: true } } )); @@ -155,7 +156,9 @@ export class BrowserEditor extends EditorPane { private _navigationBar!: BrowserNavigationBar; private _browserContainer!: HTMLElement; + private _placeholderScreenshot!: HTMLElement; private _errorContainer!: HTMLElement; + private _welcomeContainer!: HTMLElement; private _canGoBackContext!: IContextKey; private _canGoForwardContext!: IContextKey; private _storageScopeContext!: IContextKey; @@ -218,11 +221,19 @@ export class BrowserEditor extends EditorPane { this._browserContainer.tabIndex = 0; // make focusable root.appendChild(this._browserContainer); + // Create placeholder screenshot (background placeholder when WebContentsView is hidden) + this._placeholderScreenshot = $('.browser-placeholder-screenshot'); + this._browserContainer.appendChild(this._placeholderScreenshot); + // Create error container (hidden by default) this._errorContainer = $('.browser-error-container'); this._errorContainer.style.display = 'none'; this._browserContainer.appendChild(this._errorContainer); + // Create welcome container (shown when no URL is loaded) + this._welcomeContainer = this.createWelcomeContainer(); + this._browserContainer.appendChild(this._welcomeContainer); + this._register(addDisposableListener(this._browserContainer, EventType.FOCUS, (event) => { // When the browser container gets focus, make sure the browser view also gets focused. // But only if focus was already in the workbench (and not e.g. clicking back into the workbench from the browser view). @@ -362,15 +373,24 @@ export class BrowserEditor extends EditorPane { } private updateVisibility(): void { + const hasUrl = !!this._model?.url; + const hasError = !!this._model?.error; + + // Welcome container: shown when no URL is loaded + this._welcomeContainer.style.display = hasUrl ? 'none' : 'flex'; + + // Error container: shown when there's a load error + this._errorContainer.style.display = hasError ? 'flex' : 'none'; + if (this._model) { - // Blur the background image if the view is hidden due to an overlay. - this._browserContainer.classList.toggle('blur', this._editorVisible && this._overlayVisible && !this._model?.error); + // Blur the background placeholder screenshot if the view is hidden due to an overlay. + this._placeholderScreenshot.classList.toggle('blur', this._editorVisible && this._overlayVisible && !hasError); void this._model.setVisible(this.shouldShowView); } } private get shouldShowView(): boolean { - return this._editorVisible && !this._overlayVisible && !this._model?.error; + return this._editorVisible && !this._overlayVisible && !this._model?.error && !!this._model?.url; } private checkOverlays(): void { @@ -391,8 +411,7 @@ export class BrowserEditor extends EditorPane { const error: IBrowserViewLoadError | undefined = this._model.error; if (error) { - // Show error display - this._errorContainer.style.display = 'flex'; + // Update error content while (this._errorContainer.firstChild) { this._errorContainer.removeChild(this._errorContainer.firstChild); @@ -423,14 +442,16 @@ export class BrowserEditor extends EditorPane { this.setBackgroundImage(undefined); } else { - // Hide error display - this._errorContainer.style.display = 'none'; this.setBackgroundImage(this._model.screenshot); } this.updateVisibility(); } + getUrl(): string | undefined { + return this._model?.url; + } + async navigateToUrl(url: string): Promise { if (this._model) { this.group.pinEditor(this.input); // pin editor on navigation @@ -564,14 +585,44 @@ export class BrowserEditor extends EditorPane { // Update context keys for command enablement this._canGoBackContext.set(event.canGoBack); this._canGoForwardContext.set(event.canGoForward); + + // Update visibility (welcome screen, error, browser view) + this.updateVisibility(); + } + + /** + * Create the welcome container shown when no URL is loaded + */ + private createWelcomeContainer(): HTMLElement { + const container = $('.browser-welcome-container'); + const content = $('.browser-welcome-content'); + + const iconContainer = $('.browser-welcome-icon'); + iconContainer.appendChild(renderIcon(Codicon.globe)); + content.appendChild(iconContainer); + + const title = $('.browser-welcome-title'); + title.textContent = localize('browser.welcomeTitle', "Browser"); + content.appendChild(title); + + const subtitle = $('.browser-welcome-subtitle'); + subtitle.textContent = localize('browser.welcomeSubtitle', "Enter a URL above to get started."); + content.appendChild(subtitle); + + const tip = $('.browser-welcome-tip'); + tip.textContent = localize('browser.welcomeTip', "Tip: Use the Add Element to Chat feature to reference UI elements when asking Copilot for changes."); + content.appendChild(tip); + + container.appendChild(content); + return container; } private setBackgroundImage(buffer: VSBuffer | undefined): void { if (buffer) { const dataUrl = `data:image/jpeg;base64,${encodeBase64(buffer)}`; - this._browserContainer.style.backgroundImage = `url('${dataUrl}')`; + this._placeholderScreenshot.style.backgroundImage = `url('${dataUrl}')`; } else { - this._browserContainer.style.backgroundImage = ''; + this._placeholderScreenshot.style.backgroundImage = ''; } } diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditorInput.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditorInput.ts index 6a43b52152aff..57d42830dd2ae 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditorInput.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditorInput.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../base/common/codicons.js'; +import { truncate } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { BrowserViewUri } from '../../../../platform/browserView/common/browserViewUri.js'; @@ -27,6 +28,11 @@ const LOADING_SPINNER_SVG = (color: string | undefined) => ` `; +/** + * Maximum length for browser page titles before truncation + */ +const MAX_TITLE_LENGTH = 30; + /** * JSON-serializable type used during browser state serialization/deserialization */ @@ -148,6 +154,10 @@ export class BrowserEditorInput extends EditorInput { } override getName(): string { + return truncate(this.getTitle(), MAX_TITLE_LENGTH); + } + + override getTitle(): string { // Use model data if available, otherwise fall back to initial data if (this._model && this._model.url) { if (this._model.title) { diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts index d57048492afb0..6b8991df48f83 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserViewActions.ts @@ -16,6 +16,8 @@ import { BrowserViewUri } from '../../../../platform/browserView/common/browserV import { IBrowserViewWorkbenchService } from '../common/browserView.js'; import { BrowserViewStorageScope } from '../../../../platform/browserView/common/browserView.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; // Context key expression to check if browser editor is active const BROWSER_EDITOR_ACTIVE = ContextKeyExpr.equals('activeEditor', BrowserEditor.ID); @@ -55,7 +57,7 @@ class GoBackAction extends Action2 { group: 'navigation', order: 1, }, - precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_CAN_GO_BACK), + precondition: CONTEXT_BROWSER_CAN_GO_BACK, keybinding: { when: BROWSER_EDITOR_ACTIVE, weight: KeybindingWeight.WorkbenchContrib, @@ -87,9 +89,9 @@ class GoForwardAction extends Action2 { id: MenuId.BrowserNavigationToolbar, group: 'navigation', order: 2, - when: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_CAN_GO_FORWARD) + when: CONTEXT_BROWSER_CAN_GO_FORWARD }, - precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_CAN_GO_FORWARD), + precondition: CONTEXT_BROWSER_CAN_GO_FORWARD, keybinding: { when: BROWSER_EDITOR_ACTIVE, weight: KeybindingWeight.WorkbenchContrib, @@ -123,7 +125,7 @@ class ReloadAction extends Action2 { order: 3, }, keybinding: { - when: CONTEXT_BROWSER_FOCUSED, // Keybinding is only active when focus is within the browser editor + when: CONTEXT_BROWSER_FOCUSED, weight: KeybindingWeight.WorkbenchContrib + 50, // Priority over debug primary: KeyCode.F5, secondary: [KeyMod.CtrlCmd | KeyCode.KeyR], @@ -155,7 +157,16 @@ class AddElementToChatAction extends Action2 { group: 'actions', order: 1, when: ChatContextKeys.enabled - } + }, + keybinding: [{ + when: BROWSER_EDITOR_ACTIVE, + weight: KeybindingWeight.WorkbenchContrib + 50, // Priority over terminal + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyC, + }, { + when: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE), + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Escape + }] }); } @@ -174,14 +185,18 @@ class ToggleDevToolsAction extends Action2 { id: ToggleDevToolsAction.ID, title: localize2('browser.toggleDevToolsAction', 'Toggle Developer Tools'), category: BrowserCategory, - icon: Codicon.tools, + icon: Codicon.console, f1: false, toggled: ContextKeyExpr.equals(CONTEXT_BROWSER_DEVTOOLS_OPEN.key, true), menu: { id: MenuId.BrowserActionsToolbar, - group: 'actions', - order: 2, - when: BROWSER_EDITOR_ACTIVE + group: '1_developer', + order: 1, + }, + keybinding: { + when: BROWSER_EDITOR_ACTIVE, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.F12 } }); } @@ -193,6 +208,35 @@ class ToggleDevToolsAction extends Action2 { } } +class OpenInExternalBrowserAction extends Action2 { + static readonly ID = 'workbench.action.browser.openExternal'; + + constructor() { + super({ + id: OpenInExternalBrowserAction.ID, + title: localize2('browser.openExternalAction', 'Open in External Browser'), + category: BrowserCategory, + icon: Codicon.linkExternal, + f1: false, + menu: { + id: MenuId.BrowserActionsToolbar, + group: '2_export', + order: 1 + } + }); + } + + async run(accessor: ServicesAccessor, browserEditor = accessor.get(IEditorService).activeEditorPane): Promise { + if (browserEditor instanceof BrowserEditor) { + const url = browserEditor.getUrl(); + if (url) { + const openerService = accessor.get(IOpenerService); + await openerService.open(url, { openExternal: true }); + } + } + } +} + class ClearGlobalBrowserStorageAction extends Action2 { static readonly ID = 'workbench.action.browser.clearGlobalStorage'; @@ -205,7 +249,7 @@ class ClearGlobalBrowserStorageAction extends Action2 { f1: true, menu: { id: MenuId.BrowserActionsToolbar, - group: 'storage', + group: '3_settings', order: 1, when: ContextKeyExpr.equals(CONTEXT_BROWSER_STORAGE_SCOPE.key, BrowserViewStorageScope.Global) } @@ -230,7 +274,7 @@ class ClearWorkspaceBrowserStorageAction extends Action2 { f1: true, menu: { id: MenuId.BrowserActionsToolbar, - group: 'storage', + group: '3_settings', order: 2, when: ContextKeyExpr.equals(CONTEXT_BROWSER_STORAGE_SCOPE.key, BrowserViewStorageScope.Workspace) } @@ -243,6 +287,30 @@ class ClearWorkspaceBrowserStorageAction extends Action2 { } } +class OpenBrowserSettingsAction extends Action2 { + static readonly ID = 'workbench.action.browser.openSettings'; + + constructor() { + super({ + id: OpenBrowserSettingsAction.ID, + title: localize2('browser.openSettingsAction', 'Open Browser Settings'), + category: BrowserCategory, + icon: Codicon.settingsGear, + f1: false, + menu: { + id: MenuId.BrowserActionsToolbar, + group: '3_settings', + order: 3 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const preferencesService = accessor.get(IPreferencesService); + await preferencesService.openSettings({ query: '@id:workbench.browser.*,chat.sendElementsToChat.*' }); + } +} + // Register actions registerAction2(OpenIntegratedBrowserAction); registerAction2(GoBackAction); @@ -250,5 +318,7 @@ registerAction2(GoForwardAction); registerAction2(ReloadAction); registerAction2(AddElementToChatAction); registerAction2(ToggleDevToolsAction); +registerAction2(OpenInExternalBrowserAction); registerAction2(ClearGlobalBrowserStorageAction); registerAction2(ClearWorkspaceBrowserStorageAction); +registerAction2(OpenBrowserSettingsAction); diff --git a/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css b/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css index 24c5be8b5d89c..05c038bc15100 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css +++ b/src/vs/workbench/contrib/browserView/electron-browser/media/browser.css @@ -47,12 +47,20 @@ margin: 0 2px 2px; overflow: hidden; position: relative; + outline: none !important; + } + + .browser-placeholder-screenshot { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; background-image: none; background-size: contain; background-repeat: no-repeat; filter: blur(0px); transition: opacity 300ms ease-out, filter 300ms ease-out; - outline: none !important; opacity: 1.0; &.blur { @@ -105,4 +113,62 @@ font-size: 12px; } } + + .browser-welcome-container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--vscode-editor-background); + + .browser-welcome-content { + display: flex; + flex-direction: column; + align-items: center; + padding: 12px; + } + + .browser-welcome-icon { + min-height: 48px; + + .codicon { + font-size: 40px; + } + } + + .browser-welcome-title { + font-size: 24px; + margin-top: 5px; + text-align: center; + line-height: normal; + padding: 0 8px; + } + + .browser-welcome-subtitle { + position: relative; + text-align: center; + max-width: 100%; + padding: 0 20px; + margin: 8px auto 0; + color: var(--vscode-foreground); + + p { + margin-top: 8px; + margin-bottom: 8px; + } + } + + .browser-welcome-tip { + color: var(--vscode-descriptionForeground); + text-align: center; + margin: 16px auto 0; + max-width: 400px; + padding: 0 12px; + font-style: italic; + } + } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css index e113ad073ffcb..641c0d5e311e3 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css @@ -43,7 +43,6 @@ display: inline-flex; width: inherit; margin: 0 4px; - padding: 4px 8px; } .monaco-workbench .bulk-edit-panel .monaco-tl-contents { diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatAccessibilityProvider.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatAccessibilityProvider.ts index e468b666365db..90397262b5d92 100644 --- a/src/vs/workbench/contrib/chat/browser/accessibility/chatAccessibilityProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/accessibility/chatAccessibilityProvider.ts @@ -41,7 +41,7 @@ export const getToolConfirmationAlert = (accessor: ServicesAccessor, toolInvocat }; } - if (!(v.confirmationMessages?.message && state.type === IChatToolInvocation.StateKind.WaitingForConfirmation)) { + if (!(state.type === IChatToolInvocation.StateKind.WaitingForConfirmation && state.confirmationMessages?.message)) { return; } @@ -56,7 +56,7 @@ export const getToolConfirmationAlert = (accessor: ServicesAccessor, toolInvocat input = JSON.stringify(v.toolSpecificData.rawInput); } } - const titleObj = v.confirmationMessages?.title; + const titleObj = state.confirmationMessages?.title; const title = typeof titleObj === 'string' ? titleObj : titleObj?.value || ''; return { title: (title + (input ? ': ' + input : '')).trim(), diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts index f47ce1f03623c..152b390c9a7b5 100644 --- a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts @@ -99,9 +99,9 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi const toolInvocations = item.response.value.filter(item => item.kind === 'toolInvocation'); for (const toolInvocation of toolInvocations) { const state = toolInvocation.state.get(); - if (toolInvocation.confirmationMessages?.title && state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { - const title = typeof toolInvocation.confirmationMessages.title === 'string' ? toolInvocation.confirmationMessages.title : toolInvocation.confirmationMessages.title.value; - const message = typeof toolInvocation.confirmationMessages.message === 'string' ? toolInvocation.confirmationMessages.message : stripIcons(renderAsPlaintext(toolInvocation.confirmationMessages.message!)); + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation && state.confirmationMessages?.title) { + const title = typeof state.confirmationMessages.title === 'string' ? state.confirmationMessages.title : state.confirmationMessages.title.value; + const message = typeof state.confirmationMessages.message === 'string' ? state.confirmationMessages.message : stripIcons(renderAsPlaintext(state.confirmationMessages.message!)); let input = ''; if (toolInvocation.toolSpecificData) { if (toolInvocation.toolSpecificData?.kind === 'terminal') { diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatThinkingAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatThinkingAccessibleView.ts new file mode 100644 index 0000000000000..0c8e067e87509 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/accessibility/chatThinkingAccessibleView.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType } from '../../../../../platform/accessibility/browser/accessibleView.js'; +import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; +import { IChatWidgetService } from '../chat.js'; +import { IChatResponseViewModel, isResponseVM } from '../../common/model/chatViewModel.js'; + +export class ChatThinkingAccessibleView implements IAccessibleViewImplementation { + readonly priority = 105; + readonly name = 'chatThinking'; + readonly type = AccessibleViewType.View; + // Never match via the registry - this view is only opened via the explicit command (Alt+Shift+F2) + readonly when = ContextKeyExpr.false(); + + getProvider(accessor: ServicesAccessor) { + const widgetService = accessor.get(IChatWidgetService); + const widget = widgetService.lastFocusedWidget; + if (!widget) { + return; + } + + const viewModel = widget.viewModel; + if (!viewModel) { + return; + } + + // Get the latest response from the chat + const items = viewModel.getItems(); + const latestResponse = [...items].reverse().find(item => isResponseVM(item)); + if (!latestResponse || !isResponseVM(latestResponse)) { + return; + } + + // Extract thinking content from the response + const thinkingContent = this._extractThinkingContent(latestResponse); + if (!thinkingContent) { + return; + } + + return new AccessibleContentProvider( + AccessibleViewProviderId.ChatThinking, + { type: AccessibleViewType.View, id: AccessibleViewProviderId.ChatThinking, language: 'markdown' }, + () => thinkingContent, + () => widget.focusInput(), + AccessibilityVerbositySettingId.Chat + ); + } + + private _extractThinkingContent(response: IChatResponseViewModel): string | undefined { + const thinkingParts: string[] = []; + for (const part of response.response.value) { + if (part.kind === 'thinking') { + const value = Array.isArray(part.value) ? part.value.join('') : (part.value || ''); + const trimmed = value.trim(); + if (trimmed) { + thinkingParts.push(trimmed); + } + } + } + + if (thinkingParts.length === 0) { + return undefined; + } + return thinkingParts.join('\n\n'); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts index f1ec099751c11..51badfa9692ae 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts @@ -6,15 +6,19 @@ import { alert } from '../../../../../base/browser/ui/aria/aria.js'; import { localize } from '../../../../../nls.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { IChatWidgetService } from '../chat.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { isResponseVM } from '../../common/model/chatViewModel.js'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../../platform/accessibility/common/accessibility.js'; +import { IAccessibleViewService } from '../../../../../platform/accessibility/browser/accessibleView.js'; +import { ChatThinkingAccessibleView } from '../accessibility/chatThinkingAccessibleView.js'; +import { CHAT_CATEGORY } from './chatActions.js'; export const ACTION_ID_FOCUS_CHAT_CONFIRMATION = 'workbench.action.chat.focusConfirmation'; +export const ACTION_ID_OPEN_THINKING_ACCESSIBLE_VIEW = 'workbench.action.chat.openThinkingAccessibleView'; class AnnounceChatConfirmationAction extends Action2 { constructor() { @@ -67,6 +71,39 @@ class AnnounceChatConfirmationAction extends Action2 { } } +class OpenThinkingAccessibleViewAction extends Action2 { + constructor() { + super({ + id: ACTION_ID_OPEN_THINKING_ACCESSIBLE_VIEW, + title: { value: localize('openThinkingAccessibleView', 'Open Thinking Accessible View'), original: 'Open Thinking Accessible View' }, + category: CHAT_CATEGORY, + precondition: ChatContextKeys.enabled, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.Alt | KeyMod.Shift | KeyCode.F3, + when: ChatContextKeys.inChatSession + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const accessibleViewService = accessor.get(IAccessibleViewService); + const instantiationService = accessor.get(IInstantiationService); + + const thinkingView = new ChatThinkingAccessibleView(); + const provider = instantiationService.invokeFunction(thinkingView.getProvider.bind(thinkingView)); + + if (!provider) { + alert(localize('noThinking', 'No thinking')); + return; + } + + accessibleViewService.show(provider); + } +} + export function registerChatAccessibilityActions(): void { registerAction2(AnnounceChatConfirmationAction); + registerAction2(OpenThinkingAccessibleViewAction); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index 1a3a49fac76c0..9c42a79f6ef4f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -17,6 +17,7 @@ import { TerminalContribCommandId } from '../../../terminal/terminalContribExpor import { ChatContextKeyExprs, ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; import { FocusAgentSessionsAction } from '../agentSessions/agentSessionsActions.js'; +import { ACTION_ID_OPEN_THINKING_ACCESSIBLE_VIEW } from './chatAccessibilityActions.js'; import { IChatWidgetService } from '../chat.js'; import { ChatEditingShowChangesAction, ViewPreviousEditsAction } from '../chatEditing/chatEditingActions.js'; @@ -75,6 +76,7 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'age content.push(localize('chat.requestHistory', 'In the input box, use up and down arrows to navigate your request history. Edit input and use enter or the submit button to run a new request.')); content.push(localize('chat.attachments.removal', 'To remove attached contexts, focus an attachment and press Delete or Backspace.')); content.push(localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view{0}.', '')); + content.push(localize('chat.openThinkingAccessibleView', 'To inspect thinking content from the latest response, invoke the Open Thinking Accessible View command{0}.', ``)); content.push(localize('workbench.action.chat.focus', 'To focus the chat request and response list, invoke the Focus Chat command{0}. This will move focus to the most recent response, which you can then navigate using the up and down arrow keys.', getChatFocusKeybindingLabel(keybindingService, type, 'last'))); content.push(localize('workbench.action.chat.focusLastFocusedItem', 'To return to the last chat response you focused, invoke the Focus Last Focused Chat Response command{0}.', getChatFocusKeybindingLabel(keybindingService, type, 'lastFocused'))); content.push(localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command{0}.', getChatFocusKeybindingLabel(keybindingService, type, 'input'))); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index df91b07dbd858..a34abcfa1ff7f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -1194,28 +1194,3 @@ registerAction2(class ToggleChatViewTitleAction extends Action2 { await configurationService.updateValue(ChatConfiguration.ChatViewTitleEnabled, !chatViewTitleEnabled); } }); - -registerAction2(class ToggleChatViewWelcomeAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.toggleChatViewWelcome', - title: localize2('chat.toggleChatViewWelcome.label', "Show Welcome"), - category: CHAT_CATEGORY, - precondition: ChatContextKeys.enabled, - toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewWelcomeEnabled}`, true), - menu: { - id: MenuId.ChatWelcomeContext, - group: '1_modify', - order: 3, - when: ChatContextKeys.inChatEditor.negate() - } - }); - } - - async run(accessor: ServicesAccessor): Promise { - const configurationService = accessor.get(IConfigurationService); - - const chatViewWelcomeEnabled = configurationService.getValue(ChatConfiguration.ChatViewWelcomeEnabled); - await configurationService.updateValue(ChatConfiguration.ChatViewWelcomeEnabled, !chatViewWelcomeEnabled); - } -}); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts index 8accd14a1796e..9c0d2e6a662fd 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts @@ -26,6 +26,7 @@ import { IActionViewItemService } from '../../../../../platform/actions/browser/ import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ChatConfiguration } from '../../common/constants.js'; +import { AuxiliaryBarMaximizedContext } from '../../../../common/contextkeys.js'; //#region Actions and Menus @@ -80,7 +81,8 @@ MenuRegistry.appendMenuItem(MenuId.AgentSessionsToolbar, { order: 5, when: ContextKeyExpr.and( ChatContextKeys.agentSessionsViewerOrientation.isEqualTo(AgentSessionsViewerOrientation.Stacked), - ChatContextKeys.agentSessionsViewerPosition.isEqualTo(AgentSessionsViewerPosition.Right) + ChatContextKeys.agentSessionsViewerPosition.isEqualTo(AgentSessionsViewerPosition.Right), + AuxiliaryBarMaximizedContext.negate() ) }); @@ -94,7 +96,8 @@ MenuRegistry.appendMenuItem(MenuId.AgentSessionsToolbar, { order: 5, when: ContextKeyExpr.and( ChatContextKeys.agentSessionsViewerOrientation.isEqualTo(AgentSessionsViewerOrientation.Stacked), - ChatContextKeys.agentSessionsViewerPosition.isEqualTo(AgentSessionsViewerPosition.Left) + ChatContextKeys.agentSessionsViewerPosition.isEqualTo(AgentSessionsViewerPosition.Left), + AuxiliaryBarMaximizedContext.negate() ) }); @@ -108,7 +111,8 @@ MenuRegistry.appendMenuItem(MenuId.AgentSessionsToolbar, { order: 5, when: ContextKeyExpr.and( ChatContextKeys.agentSessionsViewerOrientation.isEqualTo(AgentSessionsViewerOrientation.SideBySide), - ChatContextKeys.agentSessionsViewerPosition.isEqualTo(AgentSessionsViewerPosition.Right) + ChatContextKeys.agentSessionsViewerPosition.isEqualTo(AgentSessionsViewerPosition.Right), + AuxiliaryBarMaximizedContext.negate() ) }); @@ -122,7 +126,8 @@ MenuRegistry.appendMenuItem(MenuId.AgentSessionsToolbar, { order: 5, when: ContextKeyExpr.and( ChatContextKeys.agentSessionsViewerOrientation.isEqualTo(AgentSessionsViewerOrientation.SideBySide), - ChatContextKeys.agentSessionsViewerPosition.isEqualTo(AgentSessionsViewerPosition.Left) + ChatContextKeys.agentSessionsViewerPosition.isEqualTo(AgentSessionsViewerPosition.Left), + AuxiliaryBarMaximizedContext.negate() ) }); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index 50f7d1bfc066e..623f132ddf821 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -29,7 +29,7 @@ import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { AgentSessionsPicker } from './agentSessionsPicker.js'; -import { ActiveEditorContext } from '../../../../common/contextkeys.js'; +import { ActiveEditorContext, AuxiliaryBarMaximizedContext } from '../../../../common/contextkeys.js'; import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; @@ -69,7 +69,6 @@ MenuRegistry.appendMenuItem(MenuId.ChatWelcomeContext, { when: ChatContextKeys.inChatEditor.negate() }); - export class SetAgentSessionsOrientationStackedAction extends Action2 { constructor() { @@ -77,7 +76,10 @@ export class SetAgentSessionsOrientationStackedAction extends Action2 { id: 'workbench.action.chat.setAgentSessionsOrientationStacked', title: localize2('chat.sessionsOrientation.stacked', "Stacked"), toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsOrientation}`, 'stacked'), - precondition: ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsEnabled}`, true), + precondition: ContextKeyExpr.and( + ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsEnabled}`, true), + AuxiliaryBarMaximizedContext.negate() + ), menu: { id: agentSessionsOrientationSubmenu, group: 'navigation', @@ -100,7 +102,10 @@ export class SetAgentSessionsOrientationSideBySideAction extends Action2 { id: 'workbench.action.chat.setAgentSessionsOrientationSideBySide', title: localize2('chat.sessionsOrientation.sideBySide', "Side by Side"), toggled: ContextKeyExpr.notEquals(`config.${ChatConfiguration.ChatViewSessionsOrientation}`, 'stacked'), - precondition: ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsEnabled}`, true), + precondition: ContextKeyExpr.and( + ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewSessionsEnabled}`, true), + AuxiliaryBarMaximizedContext.negate() + ), menu: { id: agentSessionsOrientationSubmenu, group: 'navigation', @@ -826,6 +831,7 @@ export class ShowAgentSessionsSidebar extends UpdateChatViewWidthAction { precondition: ContextKeyExpr.and( ChatContextKeys.enabled, ChatContextKeys.agentSessionsViewerOrientation.isEqualTo(AgentSessionsViewerOrientation.Stacked), + AuxiliaryBarMaximizedContext.negate() ), f1: true, category: CHAT_CATEGORY, @@ -849,6 +855,7 @@ export class HideAgentSessionsSidebar extends UpdateChatViewWidthAction { precondition: ContextKeyExpr.and( ChatContextKeys.enabled, ChatContextKeys.agentSessionsViewerOrientation.isEqualTo(AgentSessionsViewerOrientation.SideBySide), + AuxiliaryBarMaximizedContext.negate() ), f1: true, category: CHAT_CATEGORY, @@ -869,7 +876,10 @@ export class ToggleAgentSessionsSidebar extends Action2 { super({ id: ToggleAgentSessionsSidebar.ID, title: ToggleAgentSessionsSidebar.TITLE, - precondition: ChatContextKeys.enabled, + precondition: ContextKeyExpr.and( + ChatContextKeys.enabled, + AuxiliaryBarMaximizedContext.negate() + ), f1: true, category: CHAT_CATEGORY, }); diff --git a/src/vs/workbench/contrib/chat/browser/attachments/media/simpleBrowserOverlay.css b/src/vs/workbench/contrib/chat/browser/attachments/media/simpleBrowserOverlay.css index 3a5e84b1fc9b5..9975b3a93b8f5 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/media/simpleBrowserOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/attachments/media/simpleBrowserOverlay.css @@ -40,7 +40,6 @@ } .element-selection-main-content .monaco-button-dropdown > .monaco-button.monaco-text-button { - height: 24px; align-content: center; padding: 0px 5px; } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 5647d285ea38b..529e2d3890da9 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -113,6 +113,7 @@ import { ChatPasteProvidersFeature } from './widget/input/editor/chatPasteProvid import { QuickChatService } from './widgetHosts/chatQuick.js'; import { ChatResponseAccessibleView } from './accessibility/chatResponseAccessibleView.js'; import { ChatTerminalOutputAccessibleView } from './accessibility/chatTerminalOutputAccessibleView.js'; +import { ChatThinkingAccessibleView } from './accessibility/chatThinkingAccessibleView.js'; import { ChatSetupContribution, ChatTeardownContribution } from './chatSetup/chatSetupContributions.js'; import { ChatStatusBarEntry } from './chatStatus/chatStatusEntry.js'; import { ChatVariablesService } from './attachments/chatVariables.js'; @@ -372,11 +373,6 @@ configurationRegistry.registerConfiguration({ enum: ['inline', 'hover', 'input', 'none'], default: 'inline', }, - [ChatConfiguration.ChatViewWelcomeEnabled]: { - type: 'boolean', - default: true, - description: nls.localize('chat.welcome.enabled', "Show welcome banner when chat is empty."), - }, [ChatConfiguration.ChatViewSessionsEnabled]: { type: 'boolean', default: true, @@ -1089,6 +1085,7 @@ class ToolReferenceNamesContribution extends Disposable implements IWorkbenchCon } AccessibleViewRegistry.register(new ChatTerminalOutputAccessibleView()); +AccessibleViewRegistry.register(new ChatThinkingAccessibleView()); AccessibleViewRegistry.register(new ChatResponseAccessibleView()); AccessibleViewRegistry.register(new PanelChatAccessibilityHelp()); AccessibleViewRegistry.register(new QuickChatAccessibilityHelp()); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts index bda048be3019b..72074bcfcc6e5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingCodeEditorIntegration.ts @@ -749,11 +749,18 @@ class DiffHunkWidget implements IOverlayWidget, IModifiedFileEntryChangeHunk { arg: this, }, actionViewItemProvider: (action, options) => { + const isPrimary = action.id === 'chatEditor.action.acceptHunk'; if (!action.class) { return new class extends ActionViewItem { constructor() { super(undefined, action, { ...options, keybindingNotRenderedWithLabel: true /* hide keybinding for actions without icon */, icon: false, label: true }); } + override render(container: HTMLElement): void { + super.render(container); + if (isPrimary) { + this.element?.classList.add('primary'); + } + } }; } return undefined; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts index f7d6bef2bf164..ff4e50795c633 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts @@ -239,6 +239,7 @@ class ChatEditorOverlayWidget extends Disposable { super.render(container); if (action.id === AcceptAction.ID) { + this.element?.classList.add('primary'); const listener = this._store.add(new MutableDisposable()); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css index 0177040611daf..1033ada08b1e9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditingEditorOverlay.css @@ -4,13 +4,15 @@ *--------------------------------------------------------------------------------------------*/ .chat-editor-overlay-widget { - padding: 2px; - color: var(--vscode-button-foreground); - background-color: var(--vscode-button-background); - border-radius: 2px; + padding: 2px 4px; + color: var(--vscode-foreground); + background-color: var(--vscode-editor-background); + border-radius: 6px; border: 1px solid var(--vscode-contrastBorder); display: flex; align-items: center; + justify-content: center; + gap: 4px; z-index: 10; box-shadow: 0 2px 8px var(--vscode-widget-shadow); overflow: hidden; @@ -54,23 +56,39 @@ } .chat-editor-overlay-widget .action-item > .action-label { - padding: 5px; - font-size: 12px; - border-radius: 2px; /* same as overlay widget */ + padding: 4px 6px; + font-size: 11px; + line-height: 14px; + border-radius: 4px; /* same as overlay widget */ } +.chat-editor-overlay-widget .monaco-action-bar .actions-container { + gap: 4px; +} -.chat-editor-overlay-widget .action-item:first-child > .action-label { - padding-left: 7px; +.chat-editor-overlay-widget .action-item.primary > .action-label { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); } -.chat-editor-overlay-widget .action-item:last-child > .action-label { - padding-right: 7px; +.monaco-workbench .chat-editor-overlay-widget .monaco-action-bar .action-item.primary > .action-label:hover { + background-color: var(--vscode-button-hoverBackground); } -.chat-editor-overlay-widget.busy .chat-editor-overlay-progress .codicon, -.chat-editor-overlay-widget .action-item > .action-label.codicon { - color: var(--vscode-button-foreground); +.chat-editor-overlay-widget.busy .chat-editor-overlay-progress .codicon { + color: var(--vscode-foreground); +} + +.chat-editor-overlay-widget .action-item > .action-label.codicon:not(.separator) { + color: var(--vscode-foreground); + width: 22px; /* align with default icon button dimensions */ + height: 22px; + padding: 0; + font-size: 16px; + line-height: 22px; + display: flex; + align-items: center; + justify-content: center; } .chat-diff-change-content-widget .monaco-action-bar .action-item.disabled, @@ -85,18 +103,13 @@ } } -.chat-diff-change-content-widget .action-item > .action-label { - border-radius: 2px; /* same as overlay widget */ -} - - .chat-editor-overlay-widget .action-item.label-item { font-variant-numeric: tabular-nums; } .chat-editor-overlay-widget .monaco-action-bar .action-item.label-item > .action-label, .chat-editor-overlay-widget .monaco-action-bar .action-item.label-item > .action-label:hover { - color: var(--vscode-button-foreground); + color: var(--vscode-foreground); opacity: 1; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css index 8be9fd6ba29fe..5e4b3de1ebc60 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/media/chatEditorController.css @@ -8,6 +8,8 @@ transition: opacity 0.2s ease-in-out; display: flex; box-shadow: 0 2px 8px var(--vscode-widget-shadow); + border-radius: 6px; + overflow: hidden; } .chat-diff-change-content-widget.hover { @@ -15,25 +17,43 @@ } .chat-diff-change-content-widget .monaco-action-bar { - padding: 2px; - border-radius: 2px; - background-color: var(--vscode-button-background); - color: var(--vscode-button-foreground); + padding: 4px 4px; + border-radius: 6px; + background-color: var(--vscode-editor-background); + color: var(--vscode-foreground); border: 1px solid var(--vscode-contrastBorder); + overflow: hidden; +} + +.chat-diff-change-content-widget .monaco-action-bar .actions-container { + gap: 4px; } .chat-diff-change-content-widget .monaco-action-bar .action-item .action-label { - border-radius: 2px; + border-radius: 4px; + font-size: 11px; + line-height: 14px; + padding: 4px 6px; +} + +.chat-diff-change-content-widget .monaco-action-bar .action-item.primary .action-label { + background-color: var(--vscode-button-background); color: var(--vscode-button-foreground); - padding: 2px 5px; } -.chat-diff-change-content-widget .monaco-action-bar .action-item .action-label.codicon { - width: unset; - padding: 2px; +.monaco-workbench .chat-diff-change-content-widget .monaco-action-bar .action-item.primary .action-label:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.chat-diff-change-content-widget .monaco-action-bar .action-item .action-label.codicon:not(.separator) { + width: 22px; /* align with default icon button dimensions */ + height: 22px; + padding: 0; font-size: 16px; - line-height: 16px; - color: var(--vscode-button-foreground); + line-height: 22px; + display: flex; + align-items: center; + justify-content: center; } .chat-diff-change-content-widget .monaco-action-bar .action-item .action-label.codicon[class*='codicon-'] { diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts index a377d9a5b1730..e2205448dbe87 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts @@ -497,13 +497,17 @@ export class ChatModelsViewModel extends Disposable { } }); } - for (const model of group.models) { - if (vendor.vendor === 'copilot' && model.metadata.id === 'auto') { + for (const identifier of group.modelIdentifiers) { + const metadata = this.languageModelsService.lookupLanguageModel(identifier); + if (!metadata) { + continue; + } + if (vendor.vendor === 'copilot' && metadata.id === 'auto') { continue; } models.push({ - identifier: model.identifier, - metadata: model.metadata, + identifier, + metadata, provider, }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/media/chatModelsWidget.css b/src/vs/workbench/contrib/chat/browser/chatManagement/media/chatModelsWidget.css index 2feaf2c2416dd..c70f5b6ba08a4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/media/chatModelsWidget.css +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/media/chatModelsWidget.css @@ -43,6 +43,7 @@ .models-widget .models-search-and-button-container .section-title-actions .models-add-model-button { white-space: nowrap; + padding: 4px 8px 4px 4px; } /** Table styling **/ 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 8b41b6f025e60..a9b3f78bc37e9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -916,7 +916,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ const state = toolInvocation.state.get(); description = toolInvocation.generatedTitle || toolInvocation.pastTenseMessage || toolInvocation.invocationMessage; if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { - const confirmationTitle = toolInvocation.confirmationMessages?.title; + const confirmationTitle = state.confirmationMessages?.title; const titleMessage = confirmationTitle && (typeof confirmationTitle === 'string' ? confirmationTitle : confirmationTitle.value); @@ -932,7 +932,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } } - return renderAsPlaintext(description, { useLinkFormatter: true }); + return description ? renderAsPlaintext(description, { useLinkFormatter: true }) : ''; } public async getOrCreateChatSession(sessionResource: URI, token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 9c7599f24b1e2..b5c42764ddfee 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -41,7 +41,7 @@ import { ConfirmedReason, IChatService, IChatToolInvocation, ToolConfirmKind } f import { ChatRequestToolReferenceEntry, toToolSetVariableEntry, toToolVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { ChatConfiguration } from '../../common/constants.js'; import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js'; -import { CountTokensCallback, createToolSchemaUri, ILanguageModelToolsService, IPreparedToolInvocation, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, SpecedToolAliases, stringifyPromptTsxPart, ToolDataSource, ToolSet, VSCodeToolReference } from '../../common/tools/languageModelToolsService.js'; +import { CountTokensCallback, createToolSchemaUri, IBeginToolCallOptions, ILanguageModelToolsService, IPreparedToolInvocation, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, SpecedToolAliases, stringifyPromptTsxPart, ToolDataSource, ToolSet, VSCodeToolReference } from '../../common/tools/languageModelToolsService.js'; import { getToolConfirmationAlert } from '../accessibility/chatAccessibilityProvider.js'; const jsonSchemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); @@ -94,6 +94,9 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private readonly _callsByRequestId = new Map(); + /** Pending tool calls in the streaming phase, keyed by toolCallId */ + private readonly _pendingToolCalls = new Map(); + private readonly _isAgentModeEnabled: IObservable; constructor( @@ -196,6 +199,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo super.dispose(); this._callsByRequestId.forEach(calls => calls.forEach(call => call.store.dispose())); + this._pendingToolCalls.clear(); this._ctxToolsCount.reset(); } @@ -321,8 +325,22 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } } - // Shortcut to write to the model directly here, but could call all the way back to use the real stream. + // Check if there's an existing pending tool call from streaming phase + // Try both the callId and the chatStreamToolCallId (if provided) as lookup keys + let pendingToolCallKey: string | undefined; let toolInvocation: ChatToolInvocation | undefined; + if (this._pendingToolCalls.has(dto.callId)) { + pendingToolCallKey = dto.callId; + toolInvocation = this._pendingToolCalls.get(dto.callId); + } else if (dto.chatStreamToolCallId && this._pendingToolCalls.has(dto.chatStreamToolCallId)) { + pendingToolCallKey = dto.chatStreamToolCallId; + toolInvocation = this._pendingToolCalls.get(dto.chatStreamToolCallId); + } + const hadPendingInvocation = !!toolInvocation; + if (hadPendingInvocation && pendingToolCallKey) { + // Remove from pending since we're now invoking it + this._pendingToolCalls.delete(pendingToolCallKey); + } let requestId: string | undefined; let store: DisposableStore | undefined; @@ -367,15 +385,21 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo preparedInvocation = await this.prepareToolInvocation(tool, dto, token); prepareTimeWatch.stop(); - toolInvocation = new ChatToolInvocation(preparedInvocation, tool.data, dto.callId, dto.fromSubAgent, dto.parameters); + if (hadPendingInvocation && toolInvocation) { + // Transition from streaming to executing/waiting state + toolInvocation.transitionFromStreaming(preparedInvocation, dto.parameters); + } else { + // Create a new tool invocation (no streaming phase) + toolInvocation = new ChatToolInvocation(preparedInvocation, tool.data, dto.callId, dto.fromSubAgent, dto.parameters); + this._chatService.appendProgress(request, toolInvocation); + } + trackedCall.invocation = toolInvocation; const autoConfirmed = await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters); if (autoConfirmed) { IChatToolInvocation.confirmWith(toolInvocation, autoConfirmed); } - this._chatService.appendProgress(request, toolInvocation); - dto.toolSpecificData = toolInvocation?.toolSpecificData; if (preparedInvocation?.confirmationMessages?.title) { if (!IChatToolInvocation.executionConfirmedOrDenied(toolInvocation) && !autoConfirmed) { @@ -553,6 +577,81 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return prepared; } + beginToolCall(options: IBeginToolCallOptions): IChatToolInvocation | undefined { + // First try to look up by tool ID (the package.json "name" field), + // then fall back to looking up by toolReferenceName + const toolEntry = this._tools.get(options.toolId); + if (!toolEntry) { + return undefined; + } + + // Create the invocation in streaming state + const invocation = ChatToolInvocation.createStreaming({ + toolCallId: options.toolCallId, + toolId: options.toolId, + toolData: toolEntry.data, + fromSubAgent: options.fromSubAgent, + chatRequestId: options.chatRequestId, + }); + + // Track the pending tool call + this._pendingToolCalls.set(options.toolCallId, invocation); + + // If we have a session, append the invocation to the chat as progress + if (options.sessionResource) { + const model = this._chatService.getSession(options.sessionResource); + if (model) { + // Find the request by chatRequestId if available, otherwise use the last request + const request = options.chatRequestId + ? model.getRequests().find(r => r.id === options.chatRequestId) + : model.getRequests().at(-1); + if (request) { + this._chatService.appendProgress(request, invocation); + } + } + } + + // Call handleToolStream to get initial streaming message + this._callHandleToolStream(toolEntry, invocation, options.toolCallId, undefined, CancellationToken.None); + + return invocation; + } + + private async _callHandleToolStream(toolEntry: IToolEntry, invocation: ChatToolInvocation, toolCallId: string, rawInput: unknown, token: CancellationToken): Promise { + if (!toolEntry.impl?.handleToolStream) { + return; + } + try { + const result = await toolEntry.impl.handleToolStream({ + toolCallId, + rawInput, + chatRequestId: invocation.chatRequestId, + }, token); + + if (result?.invocationMessage) { + invocation.updateStreamingMessage(result.invocationMessage); + } + } catch (error) { + this._logService.error(`[LanguageModelToolsService#_callHandleToolStream] Error calling handleToolStream for tool ${toolEntry.data.id}:`, error); + } + } + + async updateToolStream(toolCallId: string, partialInput: unknown, token: CancellationToken): Promise { + const invocation = this._pendingToolCalls.get(toolCallId); + if (!invocation) { + return; + } + + // Update the partial input on the invocation + invocation.updatePartialInput(partialInput); + + // Call handleToolStream if the tool implements it + const toolEntry = this._tools.get(invocation.toolId); + if (toolEntry) { + await this._callHandleToolStream(toolEntry, invocation, toolCallId, partialInput, token); + } + } + private playAccessibilitySignal(toolInvocations: ChatToolInvocation[]): void { const autoApproved = this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove); if (autoApproved) { @@ -744,6 +843,13 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo calls.forEach(call => call.store.dispose()); this._callsByRequestId.delete(requestId); } + + // Clean up any pending tool calls that belong to this request + for (const [toolCallId, invocation] of this._pendingToolCalls) { + if (invocation.chatRequestId === requestId) { + this._pendingToolCalls.delete(toolCallId); + } + } } private static readonly githubMCPServerAliases = ['github/github-mcp-server', 'io.github.github/github-mcp-server', 'github-mcp-server']; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatConfirmationWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatConfirmationWidget.ts index 3299709537451..d0fea5112929b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatConfirmationWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatConfirmationWidget.ts @@ -162,7 +162,7 @@ abstract class BaseSimpleChatConfirmationWidget extends Disposable { // Create buttons buttons.forEach(buttonData => { - const buttonOptions: IButtonOptions = { ...defaultButtonStyles, secondary: buttonData.isSecondary, title: buttonData.tooltip, disabled: buttonData.disabled }; + const buttonOptions: IButtonOptions = { ...defaultButtonStyles, small: true, secondary: buttonData.isSecondary, title: buttonData.tooltip, disabled: buttonData.disabled }; let button: IButton; if (buttonData.moreActions) { @@ -363,7 +363,7 @@ abstract class BaseChatConfirmationWidget extends Disposable { this._buttonsDomNode.children[0].remove(); } for (const buttonData of buttons) { - const buttonOptions: IButtonOptions = { ...defaultButtonStyles, secondary: buttonData.isSecondary, title: buttonData.tooltip, disabled: buttonData.disabled }; + const buttonOptions: IButtonOptions = { ...defaultButtonStyles, small: true, secondary: buttonData.isSecondary, title: buttonData.tooltip, disabled: buttonData.disabled }; let button: IButton; if (buttonData.moreActions) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts index 182e0a5ad6dfd..e9436e9ad6568 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts @@ -32,6 +32,7 @@ export class ChatProgressContentPart extends Disposable implements IChatContentP private readonly showSpinner: boolean; private readonly isHidden: boolean; private readonly renderedMessage = this._register(new MutableDisposable()); + private currentContent: IMarkdownString; constructor( progress: IChatProgressMessage | IChatTask | IChatTaskSerialized, @@ -46,6 +47,7 @@ export class ChatProgressContentPart extends Disposable implements IChatContentP @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); + this.currentContent = progress.content; const followingContent = context.content.slice(context.contentIndex + 1); this.showSpinner = forceShowSpinner ?? shouldShowSpinner(followingContent, context.element); @@ -101,6 +103,12 @@ export class ChatProgressContentPart extends Disposable implements IChatContentP // Needs rerender when spinner state changes const showSpinner = shouldShowSpinner(followingContent, element); + + // Needs rerender when content changes + if (other.kind === 'progressMessage' && other.content.value !== this.currentContent.value) { + return false; + } + return other.kind === 'progressMessage' && this.showSpinner === showSpinner; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index 826547da30365..d26cbe3869fb1 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { $, clearNode, hide } from '../../../../../../base/browser/dom.js'; +import { alert } from '../../../../../../base/browser/ui/aria/aria.js'; import { IChatMarkdownContent, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService/chatService.js'; import { IChatContentPartRenderContext, IChatContentPart } from './chatContentParts.js'; import { IChatRendererContent } from '../../../common/model/chatViewModel.js'; @@ -129,6 +130,9 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen } this.currentThinkingValue = initialText; + // Alert screen reader users that thinking has started + alert(localize('chat.thinking.started', 'Thinking')); + if (configuredMode === ThinkingDisplayMode.Collapsed) { this.setExpanded(false); } else { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatConfirmationWidget.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatConfirmationWidget.css index 33ddd1bbb32e9..be0ea2424f369 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatConfirmationWidget.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatConfirmationWidget.css @@ -12,13 +12,6 @@ position: relative; } -.chat-confirmation-widget .monaco-text-button { - padding: 0 12px; - min-height: 2em; - box-sizing: border-box; - font-size: var(--vscode-chat-font-size-body-m); -} - .chat-confirmation-widget:not(:last-child) { margin-bottom: 16px; } @@ -279,22 +272,16 @@ .chat-confirmation-widget2 .chat-confirmation-widget-buttons { display: flex; padding: 5px 9px; - font-size: var(--vscode-chat-font-size-body-m); .chat-buttons { display: flex; - column-gap: 10px; + column-gap: 4px; align-items: center; .monaco-button { overflow-wrap: break-word; - padding: 2px 5px; width: inherit; } - - .monaco-text-button { - padding: 2px 10px; - } } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatExtensionsInstallToolSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatExtensionsInstallToolSubPart.ts index eace30b5aa18a..ce673417560e2 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatExtensionsInstallToolSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatExtensionsInstallToolSubPart.ts @@ -55,7 +55,8 @@ export class ExtensionsInstallConfirmationWidgetSubPart extends BaseChatToolInvo this._register(chatExtensionsContentPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); dom.append(this.domNode, chatExtensionsContentPart.domNode); - if (toolInvocation.state.get().type === IChatToolInvocation.StateKind.WaitingForConfirmation) { + const state = toolInvocation.state.get(); + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { const allowLabel = localize('allow', "Allow"); const allowTooltip = keybindingService.appendKeybinding(allowLabel, AcceptToolConfirmationActionId); @@ -83,8 +84,8 @@ export class ExtensionsInstallConfirmationWidgetSubPart extends BaseChatToolInvo ChatConfirmationWidget, context, { - title: toolInvocation.confirmationMessages?.title ?? localize('installExtensions', "Install Extensions"), - message: toolInvocation.confirmationMessages?.message ?? localize('installExtensionsConfirmation', "Click the Install button on the extension and then press Allow when finished."), + title: state.confirmationMessages?.title ?? localize('installExtensions', "Install Extensions"), + message: state.confirmationMessages?.message ?? localize('installExtensionsConfirmation', "Click the Install button on the extension and then press Allow when finished."), buttons, } )); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts index 1c72c6d32f45d..7947d601b762c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts @@ -100,13 +100,14 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS context.container.classList.add('from-sub-agent'); } - if (!toolInvocation.confirmationMessages?.title) { + const state = toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation || !state.confirmationMessages?.title) { throw new Error('Confirmation messages are missing'); } terminalData = migrateLegacyTerminalToolSpecificData(terminalData); - const { title, message, disclaimer, terminalCustomActions } = toolInvocation.confirmationMessages; + const { title, message, disclaimer, terminalCustomActions } = state.confirmationMessages; const autoApproveEnabled = this.configurationService.getValue(TerminalContribSettingId.EnableAutoApprove) === true; const autoApproveWarningAccepted = this.storageService.getBoolean(TerminalToolConfirmationStorageKeys.TerminalAutoApproveWarningAccepted, StorageScope.APPLICATION, false); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index 24f8727d48b4c..b36a2b71901cc 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -900,7 +900,7 @@ class ChatTerminalToolOutputSection extends Disposable { private async _createScrollableContainer(): Promise { this._scrollableContainer = this._register(new DomScrollableElement(this._outputBody, { vertical: ScrollbarVisibility.Hidden, - horizontal: ScrollbarVisibility.Auto, + horizontal: ScrollbarVisibility.Hidden, handleMouseWheel: true })); const scrollableDomNode = this._scrollableContainer.getDomNode(); @@ -908,6 +908,20 @@ class ChatTerminalToolOutputSection extends Disposable { this.domNode.appendChild(scrollableDomNode); this.updateAriaLabel(); + // Show horizontal scrollbar on hover/focus, hide otherwise to prevent flickering during streaming + this._register(dom.addDisposableListener(this.domNode, dom.EventType.MOUSE_ENTER, () => { + this._scrollableContainer?.updateOptions({ horizontal: ScrollbarVisibility.Auto }); + })); + this._register(dom.addDisposableListener(this.domNode, dom.EventType.MOUSE_LEAVE, () => { + this._scrollableContainer?.updateOptions({ horizontal: ScrollbarVisibility.Hidden }); + })); + this._register(dom.addDisposableListener(this.domNode, dom.EventType.FOCUS_IN, () => { + this._scrollableContainer?.updateOptions({ horizontal: ScrollbarVisibility.Auto }); + })); + this._register(dom.addDisposableListener(this.domNode, dom.EventType.FOCUS_OUT, () => { + this._scrollableContainer?.updateOptions({ horizontal: ScrollbarVisibility.Hidden }); + })); + // Track scroll state to enable scroll lock behavior (only for user scrolls) this._register(this._scrollableContainer.onScroll(() => { if (this._isProgrammaticScroll) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts index 859421939a044..dffa3138a9b5b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts @@ -23,7 +23,7 @@ import { IMarkdownRenderer } from '../../../../../../../platform/markdown/browse import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../../platform/markers/common/markers.js'; import { IChatToolInvocation, ToolConfirmKind } from '../../../../common/chatService/chatService.js'; import { CodeBlockModelCollection } from '../../../../common/widget/codeBlockModelCollection.js'; -import { createToolInputUri, createToolSchemaUri, ILanguageModelToolsService } from '../../../../common/tools/languageModelToolsService.js'; +import { createToolInputUri, createToolSchemaUri, ILanguageModelToolsService, IToolConfirmationMessages } from '../../../../common/tools/languageModelToolsService.js'; import { ILanguageModelToolsConfirmationService } from '../../../../common/tools/languageModelToolsConfirmationService.js'; import { AcceptToolConfirmationActionId, SkipToolConfirmationActionId } from '../../../actions/chatToolActions.js'; import { IChatCodeBlockInfo, IChatWidgetService } from '../../../chat.js'; @@ -63,7 +63,8 @@ export class ToolConfirmationSubPart extends AbstractToolConfirmationSubPart { @IChatMarkdownAnchorService private readonly chatMarkdownAnchorService: IChatMarkdownAnchorService, @ILanguageModelToolsConfirmationService private readonly confirmationService: ILanguageModelToolsConfirmationService, ) { - if (!toolInvocation.confirmationMessages?.title) { + const state = toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation || !state.confirmationMessages?.title) { throw new Error('Confirmation messages are missing'); } @@ -72,7 +73,7 @@ export class ToolConfirmationSubPart extends AbstractToolConfirmationSubPart { this.render({ allowActionId: AcceptToolConfirmationActionId, skipActionId: SkipToolConfirmationActionId, - allowLabel: toolInvocation.confirmationMessages.confirmResults ? localize('allowReview', "Allow and Review") : localize('allow', "Allow"), + allowLabel: state.confirmationMessages.confirmResults ? localize('allowReview', "Allow and Review") : localize('allow', "Allow"), skipLabel: localize('skip.detail', 'Proceed without running this tool'), partType: 'chatToolConfirmation', subtitle: typeof toolInvocation.originMessage === 'string' ? toolInvocation.originMessage : toolInvocation.originMessage?.value, @@ -86,12 +87,18 @@ export class ToolConfirmationSubPart extends AbstractToolConfirmationSubPart { protected override additionalPrimaryActions() { const actions = super.additionalPrimaryActions(); - if (this.toolInvocation.confirmationMessages?.allowAutoConfirm !== false) { + + const state = this.toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation) { + return actions; + } + + if (state.confirmationMessages?.allowAutoConfirm !== false) { // Get actions from confirmation service const confirmActions = this.confirmationService.getPreConfirmActions({ toolId: this.toolInvocation.toolId, source: this.toolInvocation.source, - parameters: this.toolInvocation.parameters + parameters: state.parameters }); for (const action of confirmActions) { @@ -110,12 +117,12 @@ export class ToolConfirmationSubPart extends AbstractToolConfirmationSubPart { }); } } - if (this.toolInvocation.confirmationMessages?.confirmResults) { + if (state.confirmationMessages?.confirmResults) { actions.unshift( { label: localize('allowSkip', 'Allow and Skip Reviewing Result'), data: () => { - this.toolInvocation.confirmationMessages!.confirmResults = undefined; + (state.confirmationMessages as IToolConfirmationMessages).confirmResults = undefined; this.confirmWith(this.toolInvocation, { type: ToolConfirmKind.UserAction }); } }, @@ -127,7 +134,11 @@ export class ToolConfirmationSubPart extends AbstractToolConfirmationSubPart { } protected createContentElement(): HTMLElement | string { - const { message, disclaimer } = this.toolInvocation.confirmationMessages!; + const state = this.toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation) { + return ''; + } + const { message, disclaimer } = state.confirmationMessages!; const toolInvocation = this.toolInvocation as IChatToolInvocation; if (typeof message === 'string' && !disclaimer) { @@ -305,8 +316,15 @@ export class ToolConfirmationSubPart extends AbstractToolConfirmationSubPart { } protected getTitle(): string { - const { title } = this.toolInvocation.confirmationMessages!; - return typeof title === 'string' ? title : title!.value; + const state = this.toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation) { + return ''; + } + const title = state.confirmationMessages?.title; + if (!title) { + return ''; + } + return typeof title === 'string' ? title : title.value; } private _makeMarkdownPart(container: HTMLElement, message: string | IMarkdownString, codeBlockRenderOptions: ICodeBlockRenderOptions) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts index 191c4e1b9142c..553a1532a3027 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts @@ -28,6 +28,7 @@ import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; import { ChatToolOutputSubPart } from './chatToolOutputPart.js'; import { ChatToolPostExecuteConfirmationPart } from './chatToolPostExecuteConfirmationPart.js'; import { ChatToolProgressSubPart } from './chatToolProgressPart.js'; +import { ChatToolStreamingSubPart } from './chatToolStreamingSubPart.js'; export class ChatToolInvocationPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; @@ -147,6 +148,12 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa return this.instantiationService.createInstance(ExtensionsInstallConfirmationWidgetSubPart, this.toolInvocation, this.context); } const state = this.toolInvocation.state.get(); + + // Handle streaming state - show streaming progress + if (state.type === IChatToolInvocation.StateKind.Streaming) { + return this.instantiationService.createInstance(ChatToolStreamingSubPart, this.toolInvocation, this.context, this.renderer); + } + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { if (this.toolInvocation.toolSpecificData?.kind === 'terminal') { return this.instantiationService.createInstance(ChatTerminalToolConfirmationSubPart, this.toolInvocation, this.toolInvocation.toolSpecificData, this.context, this.renderer, this.editorPool, this.currentWidthDelegate, this.codeBlockModelCollection, this.codeBlockStartIndex); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPostExecuteConfirmationPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPostExecuteConfirmationPart.ts index 54ba1affb724d..1c5af92390c46 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPostExecuteConfirmationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPostExecuteConfirmationPart.ts @@ -72,11 +72,16 @@ export class ChatToolPostExecuteConfirmationPart extends AbstractToolConfirmatio protected override additionalPrimaryActions() { const actions = super.additionalPrimaryActions(); + const state = this.toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForPostApproval) { + return actions; + } + // Get actions from confirmation service const confirmActions = this.confirmationService.getPostConfirmActions({ toolId: this.toolInvocation.toolId, source: this.toolInvocation.source, - parameters: this.toolInvocation.parameters + parameters: state.parameters }); for (const action of confirmActions) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolProgressPart.ts index 94c0c5a36027e..f16c95fde1916 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolProgressPart.ts @@ -36,7 +36,9 @@ export class ChatToolProgressSubPart extends BaseChatToolInvocationSubPart { } private createProgressPart(): HTMLElement { - if (IChatToolInvocation.isComplete(this.toolInvocation) && this.toolIsConfirmed && this.toolInvocation.pastTenseMessage) { + const isComplete = IChatToolInvocation.isComplete(this.toolInvocation); + + if (isComplete && this.toolIsConfirmed && this.toolInvocation.pastTenseMessage) { const key = this.getAnnouncementKey('complete'); const completionContent = this.toolInvocation.pastTenseMessage ?? this.toolInvocation.invocationMessage; const shouldAnnounce = this.toolInvocation.kind === 'toolInvocation' && this.hasMeaningfulContent(completionContent) ? this.computeShouldAnnounce(key) : false; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolStreamingSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolStreamingSubPart.ts new file mode 100644 index 0000000000000..11d0a6af7933b --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolStreamingSubPart.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../../base/browser/dom.js'; +import { IMarkdownString, MarkdownString } from '../../../../../../../base/common/htmlContent.js'; +import { autorun } from '../../../../../../../base/common/observable.js'; +import { IMarkdownRenderer } from '../../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { IChatProgressMessage, IChatToolInvocation } from '../../../../common/chatService/chatService.js'; +import { IChatCodeBlockInfo } from '../../../chat.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { ChatProgressContentPart } from '../chatProgressContentPart.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; + +/** + * Sub-part for rendering a tool invocation in the streaming state. + * This shows progress while the tool arguments are being streamed from the LM. + */ +export class ChatToolStreamingSubPart extends BaseChatToolInvocationSubPart { + public readonly domNode: HTMLElement; + + public override readonly codeblocks: IChatCodeBlockInfo[] = []; + + constructor( + toolInvocation: IChatToolInvocation, + private readonly context: IChatContentPartRenderContext, + private readonly renderer: IMarkdownRenderer, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(toolInvocation); + + this.domNode = this.createStreamingPart(); + } + + private createStreamingPart(): HTMLElement { + const container = document.createElement('div'); + + if (this.toolInvocation.kind !== 'toolInvocation') { + return container; + } + + const toolInvocation = this.toolInvocation; + const state = toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.Streaming) { + return container; + } + + // Observe streaming message changes + this._register(autorun(reader => { + const currentState = toolInvocation.state.read(reader); + if (currentState.type !== IChatToolInvocation.StateKind.Streaming) { + // State changed - clear the container DOM before triggering re-render + // This prevents the old streaming message from lingering + dom.clearNode(container); + this._onNeedsRerender.fire(); + return; + } + + // Read the streaming message + const streamingMessage = currentState.streamingMessage.read(reader); + const displayMessage = streamingMessage ?? toolInvocation.invocationMessage; + + const content: IMarkdownString = typeof displayMessage === 'string' + ? new MarkdownString().appendText(displayMessage) + : displayMessage; + + const progressMessage: IChatProgressMessage = { + kind: 'progressMessage', + content + }; + + const part = reader.store.add(this.instantiationService.createInstance( + ChatProgressContentPart, + progressMessage, + this.renderer, + this.context, + undefined, + true, + this.getIcon(), + toolInvocation + )); + + dom.reset(container, part.domNode); + + // Notify parent that content has changed + this._onDidChangeHeight.fire(); + })); + + return container; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index a8c47ca54b020..c59b8c329a2ca 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -777,6 +777,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer part.kind === 'toolInvocation' && IChatToolInvocation.isStreaming(part))) { + return false; + } + // Show if no content, only "used references", ends with a complete tool call, or ends with complete text edits and there is no incomplete tool call (edits are still being applied some time after they are all generated) const lastPart = findLast(partsToRender, part => part.kind !== 'markdownContent' || part.content.value.trim().length > 0); @@ -787,7 +792,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer part.kind === 'toolInvocation' && !IChatToolInvocation.isComplete(part))) || (lastPart.kind === 'progressTask' && lastPart.deferred.isSettled) || - lastPart.kind === 'prepareToolInvocation' || lastPart.kind === 'mcpServersStarting' + lastPart.kind === 'mcpServersStarting' ) { return true; } @@ -1291,14 +1296,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer | undefined): ChatThinkingContentPart | undefined { 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 ab2fd90106a4c..a98c158ad8c5c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2375,6 +2375,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const isSessionMenu = topLevelIsSessionMenu.read(reader); reader.store.add(scopedInstantiationService.createInstance(MenuWorkbenchButtonBar, actionsContainer, isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar, { telemetrySource: this.options.menus.telemetrySource, + small: true, menuOptions: { arg: sessionResource && (isSessionMenu ? sessionResource : { $mid: MarshalledId.ChatViewContext, 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 eb83d36d84607..122fc37518e1f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -903,10 +903,7 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-session .chat-editing-session .monaco-button { - height: 22px; width: fit-content; - padding: 2px 6px; - font-size: 12px; } .interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button:hover { @@ -1032,8 +1029,8 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .chat-editing-session .chat-editing-session-actions .monaco-button.secondary.monaco-text-button.codicon { cursor: pointer; - padding: 0 3px; - border-radius: 2px; + padding: 3px; + border-radius: 4px; display: inline-flex; } @@ -2435,7 +2432,6 @@ have to be updated for changes to the rules above, or to support more deeply nes .monaco-button { width: fit-content; - padding: 2px 11px; } .chat-quota-error-button, @@ -2757,7 +2753,6 @@ have to be updated for changes to the rules above, or to support more deeply nes .chat-buttons-container .monaco-button:not(.monaco-dropdown-button) { text-align: left; width: initial; - padding: 4px 8px; } .interactive-item-container .chat-edit-input-container { diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/widget/media/chatViewWelcome.css index 27bf0df2e0982..678b4037a901f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chatViewWelcome.css @@ -106,7 +106,6 @@ div.chat-welcome-view { .monaco-button { display: inline-block; width: initial; - padding: 4px 7px; } & > .chat-welcome-view-tips { 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 3652a5fd238ea..0b75f36b5a439 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -6,9 +6,10 @@ import './media/chatViewPane.css'; import { $, addDisposableListener, append, EventHelper, EventType, getWindow, setVisibility } from '../../../../../../base/browser/dom.js'; import { StandardMouseEvent } from '../../../../../../base/browser/mouseEvent.js'; +import { Orientation, Sash } from '../../../../../../base/browser/ui/sash/sash.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Event } from '../../../../../../base/common/event.js'; -import { MutableDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { MutableDisposable, toDisposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { MarshalledId } from '../../../../../../base/common/marshallingIds.js'; import { autorun, IReader } from '../../../../../../base/common/observable.js'; import { URI } from '../../../../../../base/common/uri.js'; @@ -57,10 +58,13 @@ import { AgentSessionsFilter } from '../../agentSessions/agentSessionsFilter.js' import { IAgentSessionsService } from '../../agentSessions/agentSessionsService.js'; import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidget.js'; import { IAgentSession } from '../../agentSessions/agentSessionsModel.js'; +import { IChatEntitlementService } from '../../../../../services/chat/common/chatEntitlementService.js'; interface IChatViewPaneState extends Partial { sessionId?: string; + sessionsViewerLimited?: boolean; + sessionsSidebarWidth?: number; } type ChatViewPaneOpenedClassification = { @@ -105,6 +109,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { @ILifecycleService lifecycleService: ILifecycleService, @IProgressService private readonly progressService: IProgressService, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, + @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -119,6 +124,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this.viewState.sessionId = undefined; // clear persisted session on fresh start } this.sessionsViewerLimited = this.viewState.sessionsViewerLimited ?? true; + this.sessionsViewerSidebarWidth = Math.max(ChatViewPane.SESSIONS_SIDEBAR_MIN_WIDTH, this.viewState.sessionsSidebarWidth ?? ChatViewPane.SESSIONS_SIDEBAR_DEFAULT_WIDTH); // Contextkeys this.chatViewLocationContext = ChatContextKeys.panelLocation.bindTo(contextKeyService); @@ -170,7 +176,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { } private updateViewPaneClasses(fromEvent: boolean): void { - const welcomeEnabled = this.configurationService.getValue(ChatConfiguration.ChatViewWelcomeEnabled) !== false; + const welcomeEnabled = !this.chatEntitlementService.sentiment.installed; // only show initially until Chat is setup this.viewPaneContainer?.classList.toggle('chat-view-welcome-enabled', welcomeEnabled); const activityBarLocationDefault = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === 'default'; @@ -208,8 +214,13 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { // Settings changes this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => { - return e.affectsConfiguration(ChatConfiguration.ChatViewWelcomeEnabled) || e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION); + return e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION); })(() => this.updateViewPaneClasses(true))); + + // Entitlement changes + this._register(this.chatEntitlementService.onDidChangeSentiment(() => { + this.updateViewPaneClasses(true); + })); } private onDidChangeAgents(): void { @@ -286,9 +297,11 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { //#region Sessions Control - private static readonly SESSIONS_LIMIT = 3; - private static readonly SESSIONS_SIDEBAR_WIDTH = 300; - private static readonly SESSIONS_SIDEBAR_VIEW_MIN_WIDTH = 300 /* default chat width */ + this.SESSIONS_SIDEBAR_WIDTH; + private static readonly SESSIONS_LIMIT = 5; + private static readonly SESSIONS_SIDEBAR_MIN_WIDTH = 200; + private static readonly SESSIONS_SIDEBAR_DEFAULT_WIDTH = 300; + private static readonly CHAT_WIDGET_DEFAULT_WIDTH = 300; + private static readonly SESSIONS_SIDEBAR_VIEW_MIN_WIDTH = this.CHAT_WIDGET_DEFAULT_WIDTH + this.SESSIONS_SIDEBAR_DEFAULT_WIDTH; private sessionsContainer: HTMLElement | undefined; private sessionsTitleContainer: HTMLElement | undefined; @@ -298,13 +311,16 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private sessionsLinkContainer: HTMLElement | undefined; private sessionsLink: Link | undefined; private sessionsCount = 0; - private sessionsViewerLimited = true; + private sessionsViewerLimited: boolean; private sessionsViewerOrientation = AgentSessionsViewerOrientation.Stacked; private sessionsViewerOrientationConfiguration: 'stacked' | 'sideBySide' = 'sideBySide'; private sessionsViewerOrientationContext: IContextKey; private sessionsViewerLimitedContext: IContextKey; private sessionsViewerVisibilityContext: IContextKey; private sessionsViewerPositionContext: IContextKey; + private sessionsViewerSidebarWidth: number; + private sessionsViewerSash: Sash | undefined; + private readonly sessionsViewerSashDisposables = this._register(new MutableDisposable()); private createSessionsControl(parent: HTMLElement): AgentSessionsControl { const that = this; @@ -822,6 +838,16 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { newSessionsViewerOrientation = width >= ChatViewPane.SESSIONS_SIDEBAR_VIEW_MIN_WIDTH ? AgentSessionsViewerOrientation.SideBySide : AgentSessionsViewerOrientation.Stacked; } + if ( + newSessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked && + width >= ChatViewPane.SESSIONS_SIDEBAR_VIEW_MIN_WIDTH && + this.getViewPositionAndLocation().location === ViewContainerLocation.AuxiliaryBar && + this.layoutService.isAuxiliaryBarMaximized() + ) { + // Always side-by-side in maximized auxiliary bar if space allows + newSessionsViewerOrientation = AgentSessionsViewerOrientation.SideBySide; + } + this.sessionsViewerOrientation = newSessionsViewerOrientation; if (newSessionsViewerOrientation === AgentSessionsViewerOrientation.SideBySide) { @@ -863,6 +889,17 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { // Ensure visibility is in sync before we layout const { visible: sessionsContainerVisible } = this.updateSessionsControlVisibility(); + + // Handle Sash (only visible in side-by-side) + if (!sessionsContainerVisible || this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { + this.sessionsViewerSashDisposables.clear(); + this.sessionsViewerSash = undefined; + } else if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.SideBySide) { + if (!this.sessionsViewerSashDisposables.value && this.viewPaneContainer) { + this.createSessionsViewerSash(this.viewPaneContainer, height, width); + } + } + if (!sessionsContainerVisible) { return { heightReduction: 0, widthReduction: 0 }; } @@ -874,9 +911,12 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { // Show as sidebar if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.SideBySide) { + const sessionsViewerSidebarWidth = this.computeEffectiveSideBySideSessionsSidebarWidth(width); + this.sessionsControlContainer.style.height = `${availableSessionsHeight}px`; - this.sessionsControlContainer.style.width = `${ChatViewPane.SESSIONS_SIDEBAR_WIDTH}px`; - this.sessionsControl.layout(availableSessionsHeight, ChatViewPane.SESSIONS_SIDEBAR_WIDTH); + this.sessionsControlContainer.style.width = `${sessionsViewerSidebarWidth}px`; + this.sessionsControl.layout(availableSessionsHeight, sessionsViewerSidebarWidth); + this.sessionsViewerSash?.layout(); heightReduction = 0; // side by side to chat widget widthReduction = this.sessionsContainer.offsetWidth; @@ -888,7 +928,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { if (this.sessionsViewerLimited) { sessionsHeight = this.sessionsCount * AgentSessionsListDelegate.ITEM_HEIGHT; } else { - sessionsHeight = (ChatViewPane.SESSIONS_LIMIT * 2 /* expand a bit to indicate more items */) * AgentSessionsListDelegate.ITEM_HEIGHT; + sessionsHeight = availableSessionsHeight; } sessionsHeight = Math.min(availableSessionsHeight, sessionsHeight); @@ -904,10 +944,64 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { return { heightReduction, widthReduction }; } + private computeEffectiveSideBySideSessionsSidebarWidth(width: number, sessionsViewerSidebarWidth = this.sessionsViewerSidebarWidth): number { + return Math.max( + ChatViewPane.SESSIONS_SIDEBAR_MIN_WIDTH, // never smaller than min width for side by side sessions + Math.min( + sessionsViewerSidebarWidth, + width - ChatViewPane.CHAT_WIDGET_DEFAULT_WIDTH // never so wide that chat widget is smaller than default width + ) + ); + } + getLastDimensions(orientation: AgentSessionsViewerOrientation): { height: number; width: number } | undefined { return this.lastDimensionsPerOrientation.get(orientation); } + private createSessionsViewerSash(container: HTMLElement, height: number, width: number): void { + const disposables = this.sessionsViewerSashDisposables.value = new DisposableStore(); + + const sash = this.sessionsViewerSash = disposables.add(new Sash(container, { + getVerticalSashLeft: () => { + const sessionsViewerSidebarWidth = this.computeEffectiveSideBySideSessionsSidebarWidth(this.lastDimensions?.width ?? width); + const { position } = this.getViewPositionAndLocation(); + if (position === Position.RIGHT) { + return (this.lastDimensions?.width ?? width) - sessionsViewerSidebarWidth; + } + + return sessionsViewerSidebarWidth; + } + }, { orientation: Orientation.VERTICAL })); + + let sashStartWidth: number | undefined; + disposables.add(sash.onDidStart(() => sashStartWidth = this.sessionsViewerSidebarWidth)); + disposables.add(sash.onDidEnd(() => sashStartWidth = undefined)); + + disposables.add(sash.onDidChange(e => { + if (sashStartWidth === undefined || !this.lastDimensions) { + return; + } + + const { position } = this.getViewPositionAndLocation(); + const delta = e.currentX - e.startX; + const newWidth = position === Position.RIGHT ? sashStartWidth - delta : sashStartWidth + delta; + + this.sessionsViewerSidebarWidth = this.computeEffectiveSideBySideSessionsSidebarWidth(this.lastDimensions.width, newWidth); + this.viewState.sessionsSidebarWidth = this.sessionsViewerSidebarWidth; + + this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); + })); + + disposables.add(sash.onDidReset(() => { + this.sessionsViewerSidebarWidth = ChatViewPane.SESSIONS_SIDEBAR_DEFAULT_WIDTH; + this.viewState.sessionsSidebarWidth = this.sessionsViewerSidebarWidth; + + if (this.lastDimensions) { + this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); + } + })); + } + //#endregion override saveState(): void { diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index b4f75cc832f1e..99b140e691209 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -450,14 +450,12 @@ export type ConfirmedReason = export interface IChatToolInvocation { readonly presentation: IPreparedToolInvocation['presentation']; readonly toolSpecificData?: IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent; - readonly confirmationMessages?: IToolConfirmationMessages; readonly originMessage: string | IMarkdownString | undefined; readonly invocationMessage: string | IMarkdownString; readonly pastTenseMessage: string | IMarkdownString | undefined; readonly source: ToolDataSource; readonly toolId: string; readonly toolCallId: string; - readonly parameters: unknown; readonly fromSubAgent?: boolean; readonly state: IObservable; generatedTitle?: string; @@ -469,6 +467,8 @@ export interface IChatToolInvocation { export namespace IChatToolInvocation { export const enum StateKind { + /** Tool call is streaming partial input from the LM */ + Streaming, WaitingForConfirmation, Executing, WaitingForPostApproval, @@ -480,12 +480,26 @@ export namespace IChatToolInvocation { type: StateKind; } - interface IChatToolInvocationWaitingForConfirmationState extends IChatToolInvocationStateBase { + export interface IChatToolInvocationStreamingState extends IChatToolInvocationStateBase { + type: StateKind.Streaming; + /** Observable partial input from the LM stream */ + readonly partialInput: IObservable; + /** Custom invocation message from handleToolStream */ + readonly streamingMessage: IObservable; + } + + /** Properties available after streaming is complete */ + interface IChatToolInvocationPostStreamState { + readonly parameters: unknown; + readonly confirmationMessages?: IToolConfirmationMessages; + } + + interface IChatToolInvocationWaitingForConfirmationState extends IChatToolInvocationStateBase, IChatToolInvocationPostStreamState { type: StateKind.WaitingForConfirmation; confirm(reason: ConfirmedReason): void; } - interface IChatToolInvocationPostConfirmState { + interface IChatToolInvocationPostConfirmState extends IChatToolInvocationPostStreamState { confirmed: ConfirmedReason; } @@ -510,12 +524,13 @@ export namespace IChatToolInvocation { contentForModel: IToolResult['content']; } - interface IChatToolInvocationCancelledState extends IChatToolInvocationStateBase { + interface IChatToolInvocationCancelledState extends IChatToolInvocationStateBase, IChatToolInvocationPostStreamState { type: StateKind.Cancelled; reason: ToolConfirmKind.Denied | ToolConfirmKind.Skipped; } export type State = + | IChatToolInvocationStreamingState | IChatToolInvocationWaitingForConfirmationState | IChatToolInvocationExecutingState | IChatToolWaitingForPostApprovalState @@ -531,7 +546,7 @@ export namespace IChatToolInvocation { } const state = invocation.state.read(reader); - if (state.type === StateKind.WaitingForConfirmation) { + if (state.type === StateKind.Streaming || state.type === StateKind.WaitingForConfirmation) { return undefined; // don't know yet } if (state.type === StateKind.Cancelled) { @@ -635,6 +650,47 @@ export namespace IChatToolInvocation { const state = invocation.state.read(reader); return state.type === StateKind.Completed || state.type === StateKind.Cancelled; } + + export function isStreaming(invocation: IChatToolInvocation | IChatToolInvocationSerialized, reader?: IReader): boolean { + if (invocation.kind === 'toolInvocationSerialized') { + return false; + } + + const state = invocation.state.read(reader); + return state.type === StateKind.Streaming; + } + + /** + * Get parameters from invocation. Returns undefined during streaming state. + */ + export function getParameters(invocation: IChatToolInvocation | IChatToolInvocationSerialized, reader?: IReader): unknown | undefined { + if (invocation.kind === 'toolInvocationSerialized') { + return undefined; // serialized invocations don't store parameters + } + + const state = invocation.state.read(reader); + if (state.type === StateKind.Streaming) { + return undefined; + } + + return state.parameters; + } + + /** + * Get confirmation messages from invocation. Returns undefined during streaming state. + */ + export function getConfirmationMessages(invocation: IChatToolInvocation | IChatToolInvocationSerialized, reader?: IReader): IToolConfirmationMessages | undefined { + if (invocation.kind === 'toolInvocationSerialized') { + return undefined; // serialized invocations don't store confirmation messages + } + + const state = invocation.state.read(reader); + if (state.type === StateKind.Streaming) { + return undefined; + } + + return state.confirmationMessages; + } } @@ -734,11 +790,6 @@ export class ChatMcpServersStarting implements IChatMcpServersStarting { } } -export interface IChatPrepareToolInvocationPart { - readonly kind: 'prepareToolInvocation'; - readonly toolName: string; -} - export type IChatProgress = | IChatMarkdownContent | IChatAgentMarkdownContentWithVulnerability @@ -765,7 +816,6 @@ export type IChatProgress = | IChatExtensionsContent | IChatPullRequestContent | IChatUndoStop - | IChatPrepareToolInvocationPart | IChatThinkingPart | IChatTaskSerialized | IChatElicitationRequest diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index e9b27d99b4c2f..5c212aba616da 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -28,7 +28,6 @@ export enum ChatConfiguration { ChatViewSessionsEnabled = 'chat.viewSessions.enabled', ChatViewSessionsOrientation = 'chat.viewSessions.orientation', ChatViewTitleEnabled = 'chat.viewTitle.enabled', - ChatViewWelcomeEnabled = 'chat.viewWelcome.enabled', SubagentToolCustomAgents = 'chat.customAgentInSubagent.enabled', ShowCodeBlockProgressAnimation = 'chat.agent.codeBlockProgress', RestoreLastPanelSession = 'chat.restoreLastPanelSession', diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 3c92e9646f19d..92567694b67e3 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -276,7 +276,7 @@ export interface ILanguageModelChatInfoOptions { export interface ILanguageModelsGroup { readonly group?: ILanguageModelsProviderGroup; - readonly models: ILanguageModelChatMetadataAndIdentifier[]; + readonly modelIdentifiers: string[]; readonly status?: { readonly message: string; readonly severity: Severity; @@ -546,11 +546,11 @@ export class LanguageModelsService implements ILanguageModelsService { const models = await this._resolveLanguageModels(provider, { silent }); if (models.length) { allModels.push(...models); - languageModelsGroups.push({ models }); + languageModelsGroups.push({ modelIdentifiers: models.map(m => m.identifier) }); } } catch (error) { languageModelsGroups.push({ - models: [], + modelIdentifiers: [], status: { message: getErrorMessage(error), severity: Severity.Error @@ -570,12 +570,12 @@ export class LanguageModelsService implements ILanguageModelsService { const models = await this._resolveLanguageModels(provider, { group: group.name, silent, configuration }); if (models.length) { allModels.push(...models); - languageModelsGroups.push({ group, models }); + languageModelsGroups.push({ group, modelIdentifiers: models.map(m => m.identifier) }); } } catch (error) { languageModelsGroups.push({ group, - models: [], + modelIdentifiers: [], status: { message: getErrorMessage(error), severity: Severity.Error diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 6115e54dbbe4a..8fecdb4ebf87e 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -29,7 +29,7 @@ import { ILogService } from '../../../../../platform/log/common/log.js'; import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js'; import { migrateLegacyTerminalToolSpecificData } from '../chat.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatResponseClearToPreviousToolInvocationReason, ElicitationState, IChatAgentMarkdownContentWithVulnerability, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatEditingSessionAction, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatModelReference, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatPrepareToolInvocationPart, IChatProgress, IChatProgressMessage, IChatPullRequestContent, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatService, IChatSessionContext, IChatSessionTiming, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, ResponseModelState, isIUsedContext } from '../chatService/chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatResponseClearToPreviousToolInvocationReason, ElicitationState, IChatAgentMarkdownContentWithVulnerability, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatEditingSessionAction, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatModelReference, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatPullRequestContent, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatService, IChatSessionContext, IChatSessionTiming, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, ResponseModelState, isIUsedContext } from '../chatService/chatService.js'; import { ChatAgentLocation, ChatModeKind } from '../constants.js'; import { IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../editing/chatEditingService.js'; import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier } from '../languageModels.js'; @@ -155,7 +155,6 @@ export type IChatProgressResponseContent = | IChatToolInvocationSerialized | IChatMultiDiffData | IChatUndoStop - | IChatPrepareToolInvocationPart | IChatElicitationRequest | IChatElicitationRequestSerialized | IChatClearToPreviousToolInvocation @@ -170,7 +169,7 @@ export type IChatProgressResponseContentSerialized = Exclude; -const nonHistoryKinds = new Set(['toolInvocation', 'toolInvocationSerialized', 'undoStop', 'prepareToolInvocation']); +const nonHistoryKinds = new Set(['toolInvocation', 'toolInvocationSerialized', 'undoStop']); function isChatProgressHistoryResponseContent(content: IChatProgressResponseContent): content is IChatProgressHistoryResponseContent { return !nonHistoryKinds.has(content.kind); } @@ -439,7 +438,6 @@ class AbstractResponse implements IResponse { case 'extensions': case 'pullRequest': case 'undoStop': - case 'prepareToolInvocation': case 'elicitation2': case 'elicitationSerialized': case 'thinking': @@ -1011,9 +1009,12 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel signal.read(r); for (const part of this._response.value) { - if (part.kind === 'toolInvocation' && part.state.read(r).type === IChatToolInvocation.StateKind.WaitingForConfirmation) { - const title = part.confirmationMessages?.title; - return title ? (isMarkdownString(title) ? title.value : title) : undefined; + if (part.kind === 'toolInvocation') { + const state = part.state.read(r); + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { + const title = state.confirmationMessages?.title; + return title ? (isMarkdownString(title) ? title.value : title) : undefined; + } } if (part.kind === 'confirmation' && !part.isUsed) { return part.title; diff --git a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts index 69acdebd98b9d..b5515039ffe9d 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts @@ -10,31 +10,59 @@ import { localize } from '../../../../../../nls.js'; import { ConfirmedReason, IChatExtensionsContent, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind, type IChatTerminalToolInvocationData } from '../../chatService/chatService.js'; import { IPreparedToolInvocation, isToolResultOutputDetails, IToolConfirmationMessages, IToolData, IToolProgressStep, IToolResult, ToolDataSource } from '../../tools/languageModelToolsService.js'; +export interface IStreamingToolCallOptions { + toolCallId: string; + toolId: string; + toolData: IToolData; + fromSubAgent?: boolean; + chatRequestId?: string; +} + export class ChatToolInvocation implements IChatToolInvocation { public readonly kind: 'toolInvocation' = 'toolInvocation'; - public readonly invocationMessage: string | IMarkdownString; + public invocationMessage: string | IMarkdownString; public readonly originMessage: string | IMarkdownString | undefined; public pastTenseMessage: string | IMarkdownString | undefined; public confirmationMessages: IToolConfirmationMessages | undefined; - public readonly presentation: IPreparedToolInvocation['presentation']; + public presentation: IPreparedToolInvocation['presentation']; public readonly toolId: string; - public readonly source: ToolDataSource; + public source: ToolDataSource; public readonly fromSubAgent: boolean | undefined; - public readonly parameters: unknown; + public parameters: unknown; public generatedTitle?: string; + public readonly chatRequestId?: string; - public readonly toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent; + public toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent; private readonly _progress = observableValue<{ message?: string | IMarkdownString; progress: number | undefined }>(this, { progress: 0 }); private readonly _state: ISettableObservable; + // Streaming-related observables + private readonly _partialInput = observableValue(this, undefined); + private readonly _streamingMessage = observableValue(this, undefined); + public get state(): IObservable { return this._state; } + /** + * Create a tool invocation in streaming state. + * Use this when the tool call is beginning to stream partial input from the LM. + */ + public static createStreaming(options: IStreamingToolCallOptions): ChatToolInvocation { + return new ChatToolInvocation(undefined, options.toolData, options.toolCallId, options.fromSubAgent, undefined, true, options.chatRequestId); + } - constructor(preparedInvocation: IPreparedToolInvocation | undefined, toolData: IToolData, public readonly toolCallId: string, fromSubAgent: boolean | undefined, parameters: unknown) { + constructor( + preparedInvocation: IPreparedToolInvocation | undefined, + toolData: IToolData, + public readonly toolCallId: string, + fromSubAgent: boolean | undefined, + parameters: unknown, + isStreaming: boolean = false, + chatRequestId?: string + ) { const defaultMessage = localize('toolInvocationMessage', "Using {0}", `"${toolData.displayName}"`); const invocationMessage = preparedInvocation?.invocationMessage ?? defaultMessage; this.invocationMessage = invocationMessage; @@ -47,26 +75,143 @@ export class ChatToolInvocation implements IChatToolInvocation { this.source = toolData.source; this.fromSubAgent = fromSubAgent; this.parameters = parameters; + this.chatRequestId = chatRequestId; - if (!this.confirmationMessages?.title) { - this._state = observableValue(this, { type: IChatToolInvocation.StateKind.Executing, confirmed: { type: ToolConfirmKind.ConfirmationNotNeeded, reason: this.confirmationMessages?.confirmationNotNeededReason }, progress: this._progress }); + if (isStreaming) { + // Start in streaming state + this._state = observableValue(this, { + type: IChatToolInvocation.StateKind.Streaming, + partialInput: this._partialInput, + streamingMessage: this._streamingMessage, + }); + } else if (!this.confirmationMessages?.title) { + this._state = observableValue(this, { + type: IChatToolInvocation.StateKind.Executing, + confirmed: { type: ToolConfirmKind.ConfirmationNotNeeded, reason: this.confirmationMessages?.confirmationNotNeededReason }, + progress: this._progress, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }); } else { this._state = observableValue(this, { type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, confirm: reason => { if (reason.type === ToolConfirmKind.Denied || reason.type === ToolConfirmKind.Skipped) { - this._state.set({ type: IChatToolInvocation.StateKind.Cancelled, reason: reason.type }, undefined); + this._state.set({ + type: IChatToolInvocation.StateKind.Cancelled, + reason: reason.type, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); } else { - this._state.set({ type: IChatToolInvocation.StateKind.Executing, confirmed: reason, progress: this._progress }, undefined); + this._state.set({ + type: IChatToolInvocation.StateKind.Executing, + confirmed: reason, + progress: this._progress, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); } } }); } } + /** + * Update the partial input observable during streaming. + */ + public updatePartialInput(input: unknown): void { + if (this._state.get().type !== IChatToolInvocation.StateKind.Streaming) { + return; // Only update in streaming state + } + this._partialInput.set(input, undefined); + } + + /** + * Update the streaming message (from handleToolStream). + */ + public updateStreamingMessage(message: string | IMarkdownString): void { + const state = this._state.get(); + if (state.type !== IChatToolInvocation.StateKind.Streaming) { + return; // Only update in streaming state + } + this._streamingMessage.set(message, undefined); + } + + /** + * Transition from streaming state to prepared/executing state. + * Called when the full tool call is ready. + */ + public transitionFromStreaming(preparedInvocation: IPreparedToolInvocation | undefined, parameters: unknown): void { + const currentState = this._state.get(); + if (currentState.type !== IChatToolInvocation.StateKind.Streaming) { + return; // Only transition from streaming state + } + + // Preserve the last streaming message if no new invocation message is provided + const lastStreamingMessage = this._streamingMessage.get(); + if (lastStreamingMessage && !preparedInvocation?.invocationMessage) { + this.invocationMessage = lastStreamingMessage; + } + + // Update fields from prepared invocation + this.parameters = parameters; + if (preparedInvocation) { + if (preparedInvocation.invocationMessage) { + this.invocationMessage = preparedInvocation.invocationMessage; + } + this.pastTenseMessage = preparedInvocation.pastTenseMessage; + this.confirmationMessages = preparedInvocation.confirmationMessages; + this.presentation = preparedInvocation.presentation; + this.toolSpecificData = preparedInvocation.toolSpecificData; + } + + // Transition to the appropriate state + if (!this.confirmationMessages?.title) { + this._state.set({ + type: IChatToolInvocation.StateKind.Executing, + confirmed: { type: ToolConfirmKind.ConfirmationNotNeeded, reason: this.confirmationMessages?.confirmationNotNeededReason }, + progress: this._progress, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); + } else { + this._state.set({ + type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + confirm: reason => { + if (reason.type === ToolConfirmKind.Denied || reason.type === ToolConfirmKind.Skipped) { + this._state.set({ + type: IChatToolInvocation.StateKind.Cancelled, + reason: reason.type, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); + } else { + this._state.set({ + type: IChatToolInvocation.StateKind.Executing, + confirmed: reason, + progress: this._progress, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); + } + } + }, undefined); + } + } + private _setCompleted(result: IToolResult | undefined, postConfirmed?: ConfirmedReason | undefined) { if (postConfirmed && (postConfirmed.type === ToolConfirmKind.Denied || postConfirmed.type === ToolConfirmKind.Skipped)) { - this._state.set({ type: IChatToolInvocation.StateKind.Cancelled, reason: postConfirmed.type }, undefined); + this._state.set({ + type: IChatToolInvocation.StateKind.Cancelled, + reason: postConfirmed.type, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); return; } @@ -76,6 +221,8 @@ export class ChatToolInvocation implements IChatToolInvocation { resultDetails: result?.toolResultDetails, postConfirmed, contentForModel: result?.content || [], + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, }, undefined); } @@ -93,6 +240,8 @@ export class ChatToolInvocation implements IChatToolInvocation { resultDetails: result?.toolResultDetails, contentForModel: result?.content || [], confirm: reason => this._setCompleted(result, reason), + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, }, undefined); } else { this._setCompleted(result); diff --git a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts index df3b644bb7420..4fdb96b6dbd1b 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts @@ -71,7 +71,6 @@ const responsePartSchema = Adapt.v { for (const part of parts) { // Write certain parts immediately to the model - if (part.kind === 'prepareToolInvocation' || part.kind === 'textEdit' || part.kind === 'notebookEdit' || part.kind === 'codeblockUri') { + if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized' || part.kind === 'textEdit' || part.kind === 'notebookEdit' || part.kind === 'codeblockUri') { if (part.kind === 'codeblockUri' && !inEdit) { inEdit = true; model.acceptResponseProgress(request, { kind: 'markdownContent', content: new MarkdownString('```\n'), fromSubagent: true }); } model.acceptResponseProgress(request, part); - // When we see a prepare tool invocation, reset markdown collection - if (part.kind === 'prepareToolInvocation') { + // When we see a tool invocation starting, reset markdown collection + if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') { markdownParts.length = 0; // Clear previously collected markdown } } else if (part.kind === 'markdownContent') { diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts index 25f2d885de298..b2c10b4d433e0 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -24,7 +24,7 @@ import { createDecorator } from '../../../../../platform/instantiation/common/in import { IProgress } from '../../../../../platform/progress/common/progress.js'; import { UserSelectedTools } from '../participants/chatAgents.js'; import { IVariableReference } from '../chatModes.js'; -import { IChatExtensionsContent, IChatTodoListContent, IChatToolInputInvocationData, type IChatTerminalToolInvocationData } from '../chatService/chatService.js'; +import { IChatExtensionsContent, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, type IChatTerminalToolInvocationData } from '../chatService/chatService.js'; import { ChatRequestToolReferenceEntry } from '../attachments/chatVariableEntries.js'; import { LanguageModelPartAudience } from '../languageModels.js'; import { PromptElementJSON, stringifyPromptElementJSON } from './promptTsxTypes.js'; @@ -132,6 +132,10 @@ export interface IToolInvocation { context: IToolInvocationContext | undefined; chatRequestId?: string; chatInteractionId?: string; + /** + * Optional tool call ID from the chat stream, used to correlate with pending streaming tool calls. + */ + chatStreamToolCallId?: string; /** * Lets us add some nicer UI to toolcalls that came from a sub-agent, but in the long run, this should probably just be rendered in a similar way to thinking text + tool call groups */ @@ -286,6 +290,18 @@ export enum ToolInvocationPresentation { HiddenAfterComplete = 'hiddenAfterComplete' } +export interface IToolInvocationStreamContext { + toolCallId: string; + rawInput: unknown; + chatRequestId?: string; + chatSessionId?: string; + chatInteractionId?: string; +} + +export interface IStreamedToolInvocation { + invocationMessage?: string | IMarkdownString; +} + export interface IPreparedToolInvocation { invocationMessage?: string | IMarkdownString; pastTenseMessage?: string | IMarkdownString; @@ -298,6 +314,7 @@ export interface IPreparedToolInvocation { export interface IToolImpl { invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise; prepareToolInvocation?(context: IToolInvocationPreparationContext, token: CancellationToken): Promise; + handleToolStream?(context: IToolInvocationStreamContext, token: CancellationToken): Promise; } export type IToolAndToolSetEnablementMap = ReadonlyMap; @@ -354,6 +371,14 @@ export class ToolSet { } +export interface IBeginToolCallOptions { + toolCallId: string; + toolId: string; + chatRequestId?: string; + sessionResource?: URI; + fromSubAgent?: boolean; +} + export const ILanguageModelToolsService = createDecorator('ILanguageModelToolsService'); export type CountTokensCallback = (input: string, token: CancellationToken) => Promise; @@ -372,6 +397,20 @@ export interface ILanguageModelToolsService { readonly toolsObservable: IObservable; getTool(id: string): IToolData | undefined; getToolByName(name: string, includeDisabled?: boolean): IToolData | undefined; + + /** + * Begin a tool call in the streaming phase. + * Creates a ChatToolInvocation in the Streaming state and appends it to the chat. + * Returns the invocation so it can be looked up later when invokeTool is called. + */ + beginToolCall(options: IBeginToolCallOptions): IChatToolInvocation | undefined; + + /** + * Update the streaming state of a pending tool call. + * Calls the tool's handleToolStream method to get a custom invocation message. + */ + updateToolStream(toolCallId: string, partialInput: unknown, token: CancellationToken): Promise; + invokeTool(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise; cancelToolCallsForRequest(requestId: string): void; /** Flush any pending tool updates to the extension hosts. */ diff --git a/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts index 87a4fc800800e..bcf006ffa271b 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts @@ -48,10 +48,10 @@ class MockLanguageModelsService implements ILanguageModelsService { vendor: vendorId, name: this.vendors.find(v => v.vendor === vendorId)?.displayName || 'Default' }, - models: [] + modelIdentifiers: [] }); } - groups[0].models.push({ identifier, metadata }); + groups[0].modelIdentifiers.push(identifier); this.modelGroups.set(vendorId, groups); } diff --git a/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts index f0029e5096f04..23db280a7e9c0 100644 --- a/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts @@ -642,11 +642,10 @@ suite('ChatResponseModel', () => { assert.strictEqual(response.confirmationAdjustedTimestamp.get(), start); // Add pending confirmation via tool invocation - const toolState = observableValue('state', { type: 0 /* IChatToolInvocation.StateKind.WaitingForConfirmation */ }); + const toolState = observableValue('state', { type: 1 /* IChatToolInvocation.StateKind.WaitingForConfirmation */, confirmationMessages: { title: 'Please confirm' } }); const toolInvocation = { kind: 'toolInvocation', invocationMessage: 'calling tool', - confirmationMessages: { title: 'Please confirm' }, state: toolState } as Partial as IChatToolInvocation; @@ -658,7 +657,7 @@ suite('ChatResponseModel', () => { assert.strictEqual(response.confirmationAdjustedTimestamp.get(), start); // Resolve confirmation - toolState.set({ type: 3 /* IChatToolInvocation.StateKind.Completed */ }, undefined); + toolState.set({ type: 4 /* IChatToolInvocation.StateKind.Completed */ }, undefined); // Now adjusted timestamp should reflect the wait time // The wait time was 2000ms. diff --git a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts index d51bb8aa8844f..2bc3f49c5d2c5 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts @@ -11,8 +11,9 @@ import { constObservable, IObservable } from '../../../../../../base/common/obse import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { IProgressStep } from '../../../../../../platform/progress/common/progress.js'; import { IVariableReference } from '../../../common/chatModes.js'; +import { IChatToolInvocation } from '../../../common/chatService/chatService.js'; import { ChatRequestToolReferenceEntry } from '../../../common/attachments/chatVariableEntries.js'; -import { CountTokensCallback, ILanguageModelToolsService, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolSet } from '../../../common/tools/languageModelToolsService.js'; +import { CountTokensCallback, IBeginToolCallOptions, ILanguageModelToolsService, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolSet } from '../../../common/tools/languageModelToolsService.js'; export class MockLanguageModelToolsService implements ILanguageModelToolsService { _serviceBrand: undefined; @@ -90,6 +91,15 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService }; } + beginToolCall(_options: IBeginToolCallOptions): IChatToolInvocation | undefined { + // Mock implementation - return undefined + return undefined; + } + + async updateToolStream(_toolCallId: string, _partialInput: unknown, _token: CancellationToken): Promise { + // Mock implementation - do nothing + } + toolSets: IObservable = constObservable([]); getToolSetByName(name: string): ToolSet | undefined { @@ -104,7 +114,7 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService throw new Error('Method not implemented.'); } - toToolAndToolSetEnablementMap(toolOrToolSetNames: readonly string[]): IToolAndToolSetEnablementMap { + toToolAndToolSetEnablementMap(toolOrToolSetNames: readonly string[], target: string | undefined): IToolAndToolSetEnablementMap { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.css b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.css index 0c378f88922e1..7b5530e7fa75e 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.css +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.css @@ -5,7 +5,7 @@ .suggest-input-container { padding: 2px 6px; - border-radius: 2px; + border-radius: 4px; } .suggest-input-container .monaco-editor-background, diff --git a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts index f83a1b60a533f..7acbf42409ffd 100644 --- a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts @@ -60,8 +60,9 @@ export class CommentFormActions implements IDisposable { secondary: !isPrimary, title, addPrimaryActionToDropdown: false, + small: true, ...defaultButtonStyles - }) : new Button(this.container, { secondary: !isPrimary, title, ...defaultButtonStyles }); + }) : new Button(this.container, { secondary: !isPrimary, title, small: true, ...defaultButtonStyles }); isPrimary = false; this._buttonElements.push(button.element); diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index 42a3076cffd55..1d42ac39101f8 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -319,10 +319,6 @@ margin: 0 10px 0 0; } -.review-widget .body .comment-additional-actions .button-bar .monaco-text-button { - padding: 4px 10px; -} - .review-widget .body .comment-additional-actions .codicon-drop-down-button { align-items: center; } @@ -425,7 +421,6 @@ .review-widget .body .comment-form-container .form-actions .monaco-text-button, .review-widget .body .edit-container .monaco-text-button { width: auto; - padding: 4px 10px; margin-left: 5px; } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 98e13ca7a8365..eb7649d45cb16 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -246,7 +246,6 @@ .extension-editor > .header > .details > .actions-status-container > .monaco-action-bar > .actions-container > .action-item > .extension-action.label, .extension-editor > .header > .details > .actions-status-container > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item .extension-action.label { - font-weight: 600; max-width: 300px; } @@ -269,17 +268,17 @@ /* single install */ .extension-editor > .header > .details > .actions-status-container > .monaco-action-bar > .actions-container > .action-item > .extension-action.label, .extension-editor > .header > .details > .actions-status-container > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item.empty > .extension-action.label { - border-radius: 2px; + border-radius: 4px; } /* split install */ .extension-editor > .header > .details > .actions-status-container > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item:not(.empty) > .extension-action.label { - border-radius: 2px 0 0 2px; + border-radius: 4px 0 0 4px; } .extension-editor > .header > .details > .actions-status-container > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item:not(.empty) > .monaco-dropdown .extension-action.label { border-left-width: 0; - border-radius: 0 2px 2px 0; + border-radius: 0 4px 4px 0; padding: 0 2px; } diff --git a/src/vs/workbench/contrib/issue/browser/media/issueReporter.css b/src/vs/workbench/contrib/issue/browser/media/issueReporter.css index bc997b189ebf3..d24fe259aa852 100644 --- a/src/vs/workbench/contrib/issue/browser/media/issueReporter.css +++ b/src/vs/workbench/contrib/issue/browser/media/issueReporter.css @@ -80,9 +80,7 @@ .issue-reporter-body .monaco-text-button { display: block; width: auto; - padding: 4px 10px; align-self: flex-end; - font-size: 13px; } .issue-reporter-body .monaco-button-dropdown { @@ -603,10 +601,6 @@ body.issue-reporter-body { line-height: 15px; /* approximate button height for vertical centering */ } -.issue-reporter-body .internal-elements .monaco-text-button { - font-size: 10px; - padding: 2px 8px; -} .issue-reporter-body .internal-elements #show-private-repo-name { align-self: flex-end; diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css index 04d78eea54aa0..ad55ce2304825 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css @@ -181,7 +181,6 @@ .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .monaco-text-button { width: initial; white-space: nowrap; - padding: 4px 14px; } .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-item-control.setting-list-hide-add-button .setting-list-new-row { diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index fc5382a2164a9..20c78c396f13e 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -378,7 +378,11 @@ } .scm-view .scm-editor-container .monaco-editor { - border-radius: 2px; + border-radius: 4px; +} + +.scm-view .scm-editor-container .monaco-editor .overflow-guard { + border-radius: 4px; } .scm-view .scm-editor { @@ -389,7 +393,7 @@ box-sizing: border-box; border: 1px solid var(--vscode-input-border, transparent); background-color: var(--vscode-input-background); - border-radius: 2px; + border-radius: 4px; } .scm-view .button-container { diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/recordings/rich/macos_zsh_omz_echo_3_times.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/recordings/rich/macos_zsh_omz_echo_3_times.ts index 984e205d9a565..fb03eb65f071b 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/recordings/rich/macos_zsh_omz_echo_3_times.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/recordings/rich/macos_zsh_omz_echo_3_times.ts @@ -137,10 +137,6 @@ export const events = [ "type": "output", "data": "" }, - { - "type": "promptInputChange", - "data": "echo a" - }, { "type": "output", "data": "\u001b]633;P;Cwd=/Users/tyriar/playground/test1\u0007\u001b]633;EnvSingleStart;0;448d50d0-70fe-4ab5-842e-132f3b1c159a;\u0007\u001b]633;EnvSingleEnd;448d50d0-70fe-4ab5-842e-132f3b1c159a;\u0007\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]633;A\u0007tyriar@Mac test1 % \u001b]633;B\u0007\u001b[K\u001b[?2004h" @@ -245,10 +241,6 @@ export const events = [ "type": "output", "data": "" }, - { - "type": "promptInputChange", - "data": "echo b" - }, { "type": "output", "data": "\u001b]633;P;Cwd=/Users/tyriar/playground/test1\u0007\u001b]633;EnvSingleStart;0;448d50d0-70fe-4ab5-842e-132f3b1c159a;\u0007\u001b]633;EnvSingleEnd;448d50d0-70fe-4ab5-842e-132f3b1c159a;\u0007\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]633;A\u0007tyriar@Mac test1 % \u001b]633;B\u0007\u001b[K\u001b[?2004h" @@ -357,10 +349,6 @@ export const events = [ "type": "output", "data": "" }, - { - "type": "promptInputChange", - "data": "echo c" - }, { "type": "output", "data": "\u001b]633;P;Cwd=/Users/tyriar/playground/test1\u0007\u001b]633;EnvSingleStart;0;448d50d0-70fe-4ab5-842e-132f3b1c159a;\u0007\u001b]633;EnvSingleEnd;448d50d0-70fe-4ab5-842e-132f3b1c159a;\u0007\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]633;A\u0007tyriar@Mac test1 % \u001b]633;B\u0007\u001b[K\u001b[?2004h" diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts index 3b847a23f8746..e9f18f15afac7 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts @@ -49,6 +49,22 @@ class ShellIntegrationTimeoutMigrationContribution extends Disposable implements } registerWorkbenchContribution2(ShellIntegrationTimeoutMigrationContribution.ID, ShellIntegrationTimeoutMigrationContribution, WorkbenchPhase.Eventually); +class OutputLocationMigrationContribution extends Disposable implements IWorkbenchContribution { + static readonly ID = 'terminal.outputLocationMigration'; + + constructor( + @IConfigurationService configurationService: IConfigurationService, + ) { + super(); + // Migrate legacy 'none' value to 'chat' + const currentValue = configurationService.getValue(TerminalChatAgentToolsSettingId.OutputLocation); + if (currentValue === 'none') { + configurationService.updateValue(TerminalChatAgentToolsSettingId.OutputLocation, 'chat'); + } + } +} +registerWorkbenchContribution2(OutputLocationMigrationContribution.ID, OutputLocationMigrationContribution, WorkbenchPhase.Eventually); + class ChatAgentToolsContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'terminal.chatAgentTools'; 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 2ac03d97b9476..6de2cb80cc32f 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 @@ -26,9 +26,9 @@ import { IConfirmationPrompt, IExecution, IPollingResult, OutputMonitorState, Po import { getTextResponseFromStream } from './utils.js'; import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; import { TerminalChatAgentToolsSettingId } from '../../../common/terminalChatAgentToolsConfiguration.js'; -import { ILogService } from '../../../../../../../platform/log/common/log.js'; import { ITerminalService } from '../../../../../terminal/browser/terminal.js'; import { LocalChatSessionUri } from '../../../../../chat/common/model/chatUri.js'; +import { ITerminalLogService } from '../../../../../../../platform/terminal/common/terminal.js'; export interface IOutputMonitor extends Disposable { readonly pollingResult: IPollingResult & { pollDurationMs: number } | undefined; @@ -94,7 +94,7 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { @IChatService private readonly _chatService: IChatService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @ILogService private readonly _logService: ILogService, + @ITerminalLogService private readonly _logService: ITerminalLogService, @ITerminalService private readonly _terminalService: ITerminalService, ) { super(); @@ -128,6 +128,7 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { const shouldContinuePolling = await this._handleTimeoutState(command, invocationContext, extended, token); if (shouldContinuePolling) { extended = true; + this._state = OutputMonitorState.PollingForIdle; continue; } else { this._promptPart?.hide(); @@ -260,63 +261,15 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { return { resources, modelOutputEvalResponse, shouldContinuePollling: false, output: custom?.output ?? output }; } - private async _handleTimeoutState(command: string, invocationContext: IToolInvocationContext | undefined, extended: boolean, token: CancellationToken): Promise { - let continuePollingPart: ChatElicitationRequestPart | undefined; - if (extended) { + private async _handleTimeoutState(_command: string, _invocationContext: IToolInvocationContext | undefined, _extended: boolean, _token: CancellationToken): Promise { + // Stop after extended polling (2 minutes) without notifying user + if (_extended) { + this._logService.info('OutputMonitor: Extended polling timeout reached after 2 minutes'); this._state = OutputMonitorState.Cancelled; return false; } - extended = true; - - const { promise: p, part } = await this._promptForMorePolling(command, token, invocationContext); - let continuePollingDecisionP: Promise | undefined = p; - continuePollingPart = part; - - // Start another polling pass and race it against the user's decision - const nextPollP = this._waitForIdle(this._execution, extended, token) - .catch((): IPollingResult => ({ - state: OutputMonitorState.Cancelled, - output: this._execution.getOutput(), - modelOutputEvalResponse: 'Cancelled' - })); - - const race = await Promise.race([ - continuePollingDecisionP.then(v => ({ kind: 'decision' as const, v })), - nextPollP.then(r => ({ kind: 'poll' as const, r })) - ]); - - if (race.kind === 'decision') { - try { continuePollingPart?.hide(); } catch { /* noop */ } - continuePollingPart = undefined; - - // User explicitly declined to keep waiting, so finish with the timed-out result - if (race.v === false) { - this._state = OutputMonitorState.Cancelled; - return false; - } - - // User accepted; keep polling (the loop iterates again). - // Clear the decision so we don't race on a resolved promise. - continuePollingDecisionP = undefined; - return true; - } else { - // A background poll completed while waiting for a decision - const r = race.r; - // r can be either an OutputMonitorState or an IPollingResult object (from catch) - const state = (typeof r === 'object' && r !== null) ? r.state : r; - - if (state === OutputMonitorState.Idle || state === OutputMonitorState.Cancelled || state === OutputMonitorState.Timeout) { - try { continuePollingPart?.hide(); } catch { /* noop */ } - continuePollingPart = undefined; - continuePollingDecisionP = undefined; - this._promptPart = undefined; - - return false; - } - - // Still timing out; loop and race again with the same prompt. - return true; - } + // Continue polling with exponential backoff + return true; } /** @@ -414,27 +367,6 @@ export class OutputMonitor extends Disposable implements IOutputMonitor { this._userInputListener = undefined; } - private async _promptForMorePolling(command: string, token: CancellationToken, context: IToolInvocationContext | undefined): Promise<{ promise: Promise; part?: ChatElicitationRequestPart }> { - if (token.isCancellationRequested || this._state === OutputMonitorState.Cancelled) { - return { promise: Promise.resolve(false) }; - } - const result = this._createElicitationPart( - token, - context?.sessionId, - new MarkdownString(localize('poll.terminal.waiting', "Continue waiting for `{0}`?", command)), - new MarkdownString(localize('poll.terminal.polling', "This will continue to poll for output to determine when the terminal becomes idle for up to 2 minutes.")), - '', - localize('poll.terminal.accept', 'Yes'), - localize('poll.terminal.reject', 'No'), - async () => true, - async () => { this._state = OutputMonitorState.Cancelled; return false; } - ); - - return { promise: result.promise.then(p => p ?? false), part: result.part }; - } - - - private async _assessOutputForErrors(buffer: string, token: CancellationToken): Promise { const model = await this._getLanguageModel(); if (!model) { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/types.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/types.ts index d4a92506c1a49..27dde5dba9cc1 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/types.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/types.ts @@ -53,6 +53,6 @@ export const enum PollingConsts { MinPollingDuration = 500, FirstPollingMaxDuration = 20000, // 20 seconds ExtendedPollingMaxDuration = 120000, // 2 minutes - MaxPollingIntervalDuration = 2000, // 2 seconds + MaxPollingIntervalDuration = 10000, // 10 seconds - grows via exponential backoff MaxRecursionCount = 5 } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index 34ba1c2f912ca..57351d2b06590 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -8,7 +8,6 @@ import type { IJSONSchema } from '../../../../../base/common/jsonSchema.js'; import { localize } from '../../../../../nls.js'; import { type IConfigurationPropertySchema } from '../../../../../platform/configuration/common/configurationRegistry.js'; import { TerminalSettingId } from '../../../../../platform/terminal/common/terminal.js'; -import product from '../../../../../platform/product/common/product.js'; import { terminalProfileBaseProperties } from '../../../../../platform/terminal/common/terminalPlatformConfiguration.js'; import { PolicyCategory } from '../../../../../base/common/policy.js'; @@ -491,14 +490,14 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary { } as any) } ); - instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(ITerminalLogService, new NullLogService()); cts = new CancellationTokenSource(); }); diff --git a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css index 42debd6aca7b3..181db2d28a943 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css +++ b/src/vs/workbench/contrib/userDataProfile/browser/media/userDataProfilesEditor.css @@ -365,7 +365,6 @@ .profiles-editor .contents-container .profile-body .profile-row-container .profile-workspaces-button-container .monaco-button { width: inherit; - padding: 2px 14px; } /* Profile Editor Tree Theming */ diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index 9b4448a62be60..fefdf4c9dfe77 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -946,11 +946,7 @@ } .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .getting-started-step .step-description-container .monaco-button { - height: 24px; width: fit-content; - display: flex; - padding: 0 11px; - align-items: center; min-width: max-content; } diff --git a/src/vs/workbench/contrib/workspace/browser/media/workspaceTrustEditor.css b/src/vs/workbench/contrib/workspace/browser/media/workspaceTrustEditor.css index ce487f6cf7bf2..3bd850fb25bf5 100644 --- a/src/vs/workbench/contrib/workspace/browser/media/workspaceTrustEditor.css +++ b/src/vs/workbench/contrib/workspace/browser/media/workspaceTrustEditor.css @@ -175,7 +175,6 @@ .workspace-trust-editor .workspace-trust-settings .trusted-uris-button-bar .monaco-button { width: fit-content; - padding: 5px 10px; overflow: hidden; text-overflow: ellipsis; outline-offset: 2px !important; @@ -188,7 +187,7 @@ } .workspace-trust-editor .workspace-trust-features .workspace-trust-buttons-row .workspace-trust-buttons .monaco-button-dropdown .monaco-dropdown-button { - padding: 5px; + padding: 0 4px; } .workspace-trust-limitations { diff --git a/src/vs/workbench/services/themes/browser/cssExtensionPoint.ts b/src/vs/workbench/services/themes/browser/cssExtensionPoint.ts new file mode 100644 index 0000000000000..cdeca23bd2dd9 --- /dev/null +++ b/src/vs/workbench/services/themes/browser/cssExtensionPoint.ts @@ -0,0 +1,328 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from '../../../../nls.js'; +import { ExtensionsRegistry, IExtensionPointUser } from '../../extensions/common/extensionsRegistry.js'; +import { isProposedApiEnabled } from '../../extensions/common/extensions.js'; +import * as resources from '../../../../base/common/resources.js'; +import { IFileService, FileChangeType } from '../../../../platform/files/common/files.js'; +import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js'; +import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; +import { FileAccess } from '../../../../base/common/network.js'; +import { createLinkElement } from '../../../../base/browser/dom.js'; +import { IWorkbenchThemeService } from '../common/workbenchThemeService.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; + +interface ICSSExtensionPoint { + path: string; +} + +const CSS_CACHE_STORAGE_KEY = 'workbench.contrib.css.cache'; + +interface ICSSCacheEntry { + extensionId: string; + cssLocations: string[]; +} + +const cssExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'css', + jsonSchema: { + description: nls.localize('contributes.css', "Contributes CSS files to be loaded in the workbench."), + type: 'array', + items: { + type: 'object', + properties: { + path: { + description: nls.localize('contributes.css.path', "Path to the CSS file. The path is relative to the extension folder."), + type: 'string' + } + }, + required: ['path'] + }, + defaultSnippets: [{ body: [{ path: '${1:styles.css}' }] }] + } +}); + +class CSSFileWatcher implements IDisposable { + + private readonly watchedLocations = new Map(); + + constructor( + private readonly fileService: IFileService, + private readonly environmentService: IBrowserWorkbenchEnvironmentService, + private readonly onUpdate: (uri: URI) => void + ) { } + + watch(uri: URI): void { + const key = uri.toString(); + if (this.watchedLocations.has(key)) { + return; + } + + if (!this.environmentService.isExtensionDevelopment) { + return; + } + + const disposables = new DisposableStore(); + disposables.add(this.fileService.watch(uri)); + disposables.add(this.fileService.onDidFilesChange(e => { + if (e.contains(uri, FileChangeType.UPDATED)) { + this.onUpdate(uri); + } + })); + this.watchedLocations.set(key, { uri, disposables }); + } + + unwatch(uri: URI): void { + const key = uri.toString(); + const entry = this.watchedLocations.get(key); + if (entry) { + entry.disposables.dispose(); + this.watchedLocations.delete(key); + } + } + + dispose(): void { + for (const entry of this.watchedLocations.values()) { + entry.disposables.dispose(); + } + this.watchedLocations.clear(); + } +} + +export class CSSExtensionPoint { + + private readonly disposables = new DisposableStore(); + private readonly stylesheetsByExtension = new Map(); + private readonly pendingExtensions = new Map>(); + private readonly watcher: CSSFileWatcher; + + constructor( + @IFileService fileService: IFileService, + @IBrowserWorkbenchEnvironmentService environmentService: IBrowserWorkbenchEnvironmentService, + @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, + @IStorageService private readonly storageService: IStorageService + ) { + this.watcher = this.disposables.add(new CSSFileWatcher(fileService, environmentService, uri => this.reloadStylesheet(uri))); + this.disposables.add(toDisposable(() => { + for (const entries of this.stylesheetsByExtension.values()) { + for (const entry of entries) { + entry.disposables.dispose(); + } + } + this.stylesheetsByExtension.clear(); + })); + + // Apply cached CSS immediately on startup if a theme from the cached extension is active + this.applyCachedCSS(); + + // Listen to theme changes to activate/deactivate CSS + this.disposables.add(this.themeService.onDidColorThemeChange(() => this.onThemeChange())); + this.disposables.add(this.themeService.onDidFileIconThemeChange(() => this.onThemeChange())); + this.disposables.add(this.themeService.onDidProductIconThemeChange(() => this.onThemeChange())); + + cssExtensionPoint.setHandler((extensions, delta) => { + // Handle removed extensions + for (const extension of delta.removed) { + const extensionId = extension.description.identifier.value; + this.pendingExtensions.delete(extensionId); + this.removeStylesheets(extensionId); + this.clearCacheForExtension(extensionId); + } + + // Handle added extensions + for (const extension of delta.added) { + if (!isProposedApiEnabled(extension.description, 'css')) { + extension.collector.error(`The '${cssExtensionPoint.name}' contribution point is proposed API.`); + continue; + } + + const extensionValue = extension.value; + const collector = extension.collector; + + if (!extensionValue || !Array.isArray(extensionValue)) { + collector.error(nls.localize('invalid.css.configuration', "'contributes.css' must be an array.")); + continue; + } + + const extensionId = extension.description.identifier.value; + + // Store the extension for later activation + this.pendingExtensions.set(extensionId, extension); + + // Check if this extension's theme is currently active + if (this.isExtensionThemeActive(extensionId)) { + this.activateExtensionCSS(extension); + } + } + }); + } + + private isExtensionThemeActive(extensionId: string): boolean { + const colorTheme = this.themeService.getColorTheme(); + const fileIconTheme = this.themeService.getFileIconTheme(); + const productIconTheme = this.themeService.getProductIconTheme(); + + return !!(colorTheme.extensionData && ExtensionIdentifier.equals(colorTheme.extensionData.extensionId, extensionId)) || + !!(fileIconTheme.extensionData && ExtensionIdentifier.equals(fileIconTheme.extensionData.extensionId, extensionId)) || + !!(productIconTheme.extensionData && ExtensionIdentifier.equals(productIconTheme.extensionData.extensionId, extensionId)); + } + + private onThemeChange(): void { + // Check all pending extensions and activate/deactivate as needed + for (const [extensionId, extension] of this.pendingExtensions) { + const isActive = this.stylesheetsByExtension.has(extensionId); + const shouldBeActive = this.isExtensionThemeActive(extensionId); + + if (shouldBeActive && !isActive) { + this.activateExtensionCSS(extension); + } else if (!shouldBeActive && isActive) { + this.removeStylesheets(extensionId); + this.clearCacheForExtension(extensionId); + } + } + } + + private activateExtensionCSS(extension: IExtensionPointUser): void { + const extensionId = extension.description.identifier.value; + + // Already activated (e.g., from cache on startup) + if (this.stylesheetsByExtension.has(extensionId)) { + return; + } + + const extensionLocation = extension.description.extensionLocation; + const extensionValue = extension.value; + const collector = extension.collector; + + const entries: { readonly uri: URI; readonly element: HTMLLinkElement; readonly disposables: DisposableStore }[] = []; + const cssLocations: string[] = []; + + for (const cssContribution of extensionValue) { + if (!cssContribution.path || typeof cssContribution.path !== 'string') { + collector.error(nls.localize('invalid.css.path', "'contributes.css.path' must be a string.")); + continue; + } + + const cssLocation = resources.joinPath(extensionLocation, cssContribution.path); + + // Validate that the CSS file is within the extension folder + if (!resources.isEqualOrParent(cssLocation, extensionLocation)) { + collector.warn(nls.localize('invalid.css.path.location', "Expected 'contributes.css.path' ({0}) to be included inside extension's folder ({1}).", cssLocation.path, extensionLocation.path)); + continue; + } + + const entryDisposables = new DisposableStore(); + const element = this.createCSSLinkElement(cssLocation, extensionId, entryDisposables); + entries.push({ uri: cssLocation, element, disposables: entryDisposables }); + cssLocations.push(cssLocation.toString()); + + // Watch for changes + this.watcher.watch(cssLocation); + } + + if (entries.length > 0) { + this.stylesheetsByExtension.set(extensionId, entries); + + // Cache the CSS locations for faster startup next time + this.cacheExtensionCSS(extensionId, cssLocations); + } + } + + private removeStylesheets(extensionId: string): void { + const entries = this.stylesheetsByExtension.get(extensionId); + if (entries) { + for (const entry of entries) { + this.watcher.unwatch(entry.uri); + entry.disposables.dispose(); + } + this.stylesheetsByExtension.delete(extensionId); + } + } + + private applyCachedCSS(): void { + const cached = this.getCachedCSS(); + if (!cached) { + return; + } + + // Check if a theme from the cached extension is active + if (!this.isExtensionThemeActive(cached.extensionId)) { + // Theme changed, invalidate the cache + this.clearCacheForExtension(cached.extensionId); + return; + } + + // Apply cached CSS immediately + const entries: { readonly uri: URI; readonly element: HTMLLinkElement; readonly disposables: DisposableStore }[] = []; + + for (const cssLocationString of cached.cssLocations) { + const cssLocation = URI.parse(cssLocationString); + const entryDisposables = new DisposableStore(); + const element = this.createCSSLinkElement(cssLocation, cached.extensionId, entryDisposables); + entries.push({ uri: cssLocation, element, disposables: entryDisposables }); + + // Watch for changes + this.watcher.watch(cssLocation); + } + + if (entries.length > 0) { + this.stylesheetsByExtension.set(cached.extensionId, entries); + } + } + + private getCachedCSS(): ICSSCacheEntry | undefined { + const raw = this.storageService.get(CSS_CACHE_STORAGE_KEY, StorageScope.PROFILE); + if (!raw) { + return undefined; + } + try { + return JSON.parse(raw); + } catch { + return undefined; + } + } + + private cacheExtensionCSS(extensionId: string, cssLocations: string[]): void { + const entry: ICSSCacheEntry = { extensionId, cssLocations }; + this.storageService.store(CSS_CACHE_STORAGE_KEY, JSON.stringify(entry), StorageScope.PROFILE, StorageTarget.MACHINE); + } + + private clearCacheForExtension(extensionId: string): void { + const cached = this.getCachedCSS(); + if (cached && ExtensionIdentifier.equals(cached.extensionId, extensionId)) { + this.storageService.remove(CSS_CACHE_STORAGE_KEY, StorageScope.PROFILE); + } + } + + private createCSSLinkElement(uri: URI, extensionId: string, disposables: DisposableStore): HTMLLinkElement { + const element = createLinkElement(); + element.rel = 'stylesheet'; + element.type = 'text/css'; + element.className = `extension-contributed-css ${extensionId}`; + element.href = FileAccess.uriToBrowserUri(uri).toString(true); + disposables.add(toDisposable(() => element.remove())); + return element; + } + + private reloadStylesheet(uri: URI): void { + const uriString = uri.toString(); + for (const entries of this.stylesheetsByExtension.values()) { + for (const entry of entries) { + if (entry.uri.toString() === uriString) { + // Cache-bust by adding a timestamp query parameter + const browserUri = FileAccess.uriToBrowserUri(uri); + entry.element.href = browserUri.with({ query: `v=${Date.now()}` }).toString(true); + } + } + } + } + + dispose(): void { + this.disposables.dispose(); + } +} diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index aa7001a3d2fe0..01dcc338f8010 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -80,9 +80,12 @@ declare module 'vscode' { constructor(value: Uri, license: string, snippet: string); } - export class ChatPrepareToolInvocationPart { - toolName: string; - constructor(toolName: string); + export interface ChatToolInvocationStreamData { + /** + * Partial or not-yet-validated arguments that have streamed from the language model. + * Tools may use this to render interim UI while the full invocation input is collected. + */ + readonly partialInput?: unknown; } export interface ChatTerminalToolInvocationData { @@ -176,7 +179,7 @@ declare module 'vscode' { constructor(uris: Uri[], callback: () => Thenable); } - export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart | ChatResponseExtensionsPart | ChatResponsePullRequestPart | ChatPrepareToolInvocationPart | ChatToolInvocationPart | ChatResponseMultiDiffPart | ChatResponseThinkingProgressPart | ChatResponseExternalEditPart; + export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart | ChatResponseExtensionsPart | ChatResponsePullRequestPart | ChatToolInvocationPart | ChatResponseMultiDiffPart | ChatResponseThinkingProgressPart | ChatResponseExternalEditPart; export class ChatResponseWarningPart { value: MarkdownString; constructor(value: string | MarkdownString); @@ -349,7 +352,21 @@ declare module 'vscode' { codeCitation(value: Uri, license: string, snippet: string): void; - prepareToolInvocation(toolName: string): void; + /** + * Begin a tool invocation in streaming mode. This creates a tool invocation that will + * display streaming progress UI until the tool is actually invoked. + * @param toolCallId Unique identifier for this tool call, used to correlate streaming updates and final invocation. + * @param toolName The name of the tool being invoked. + * @param streamData Optional initial streaming data with partial arguments. + */ + beginToolInvocation(toolCallId: string, toolName: string, streamData?: ChatToolInvocationStreamData): void; + + /** + * Update the streaming data for a tool invocation that was started with `beginToolInvocation`. + * @param toolCallId The tool call ID that was passed to `beginToolInvocation`. + * @param streamData New streaming data with updated partial arguments. + */ + updateToolInvocation(toolCallId: string, streamData: ChatToolInvocationStreamData): void; push(part: ExtendedChatResponsePart): void; @@ -668,6 +685,37 @@ declare module 'vscode' { export interface LanguageModelToolInvocationOptions { model?: LanguageModelChat; + chatStreamToolCallId?: string; + } + + export interface LanguageModelToolInvocationStreamOptions { + /** + * Raw argument payload, such as the streamed JSON fragment from the language model. + */ + readonly rawInput?: unknown; + + readonly chatRequestId?: string; + readonly chatSessionId?: string; + readonly chatInteractionId?: string; + } + + export interface LanguageModelToolStreamResult { + /** + * A customized progress message to show while the tool runs. + */ + invocationMessage?: string | MarkdownString; + } + + export interface LanguageModelTool { + /** + * Called zero or more times before {@link LanguageModelTool.prepareInvocation} while the + * language model streams argument data for the invocation. Use this to update progress + * or UI with the partial arguments that have been generated so far. + * + * Implementations must be free of side-effects and should be resilient to receiving + * malformed or incomplete input. + */ + handleToolStream?(options: LanguageModelToolInvocationStreamOptions, token: CancellationToken): ProviderResult; } export interface ChatRequest { diff --git a/src/vscode-dts/vscode.proposed.css.d.ts b/src/vscode-dts/vscode.proposed.css.d.ts new file mode 100644 index 0000000000000..3bf4c59dbae57 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.css.d.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// empty placeholder declaration for `contributes.css`