diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 4b842c07844ce..fbd06340e57b7 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1127,10 +1127,11 @@ export class Repository implements Disposable { return undefined; } - // Since we are inspecting the resource groups - // we have to ensure that the repository state - // is up to date - // await this.status(); + // Ignore path that is inside a hidden repository + if (this.isHidden === true) { + this.logger.trace(`[Repository][provideOriginalResource] Repository is hidden: ${uri.toString()}`); + return undefined; + } // Ignore path that is inside a merge group if (this.mergeGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { @@ -3293,6 +3294,12 @@ export class StagedResourceQuickDiffProvider implements QuickDiffProvider { return undefined; } + // Ignore path that is inside a hidden repository + if (this._repository.isHidden === true) { + this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Repository is hidden: ${uri.toString()}`); + return undefined; + } + // Ignore symbolic links const stat = await workspace.fs.stat(uri); if ((stat.type & FileType.SymbolicLink) !== 0) { @@ -3300,11 +3307,6 @@ export class StagedResourceQuickDiffProvider implements QuickDiffProvider { return undefined; } - // Since we are inspecting the resource groups - // we have to ensure that the repository state - // is up to date - // await this._repository.status(); - // Ignore resources that are not in the index group if (!this._repository.indexGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource is not part of a index group: ${uri.toString()}`); diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index 8b361f66f61ae..0d558eeebf654 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -58,7 +58,8 @@ "markdownDescription": "%configuration.useIntegratedBrowser.description%", "scope": "application", "tags": [ - "experimental" + "experimental", + "onExP" ] } } diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/colorizedBracketPairsDecorationProvider.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/colorizedBracketPairsDecorationProvider.ts index 2b431cb64bd60..66e78d57cb07a 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/colorizedBracketPairsDecorationProvider.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/colorizedBracketPairsDecorationProvider.ts @@ -42,7 +42,7 @@ export class ColorizedBracketPairsDecorationProvider extends Disposable implemen //#endregion - getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[] { + getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean, filterFontDecorations?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[] { if (onlyMinimapDecorations) { // Bracket pair colorization decorations are not rendered in the minimap return []; @@ -70,7 +70,7 @@ export class ColorizedBracketPairsDecorationProvider extends Disposable implemen return result; } - getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[] { + getAllDecorations(ownerId?: number, filterOutValidation?: boolean, filterFontDecorations?: boolean): IModelDecoration[] { if (ownerId === undefined) { return []; } @@ -80,7 +80,8 @@ export class ColorizedBracketPairsDecorationProvider extends Disposable implemen return this.getDecorationsInRange( new Range(1, 1, this.textModel.getLineCount(), 1), ownerId, - filterOutValidation + filterOutValidation, + filterFontDecorations ); } } diff --git a/src/vs/editor/common/model/decorationProvider.ts b/src/vs/editor/common/model/decorationProvider.ts index e8154b7277b96..e28ef209de8cc 100644 --- a/src/vs/editor/common/model/decorationProvider.ts +++ b/src/vs/editor/common/model/decorationProvider.ts @@ -15,14 +15,14 @@ export interface DecorationProvider { * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). * @return An array with the decorations */ - getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean): IModelDecoration[]; + getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean, filterFontDecorations?: boolean): IModelDecoration[]; /** * Gets all the decorations as an array. * @param ownerId If set, it will ignore decorations belonging to other owners. * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). */ - getAllDecorations(ownerId?: number, filterOutValidation?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[]; + getAllDecorations(ownerId?: number, filterOutValidation?: boolean, filterFontDecorations?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[]; } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 1953c170203a6..f38bd4218d345 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1819,8 +1819,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const range = new Range(startLineNumber, 1, endLineNumber, endColumn); const decorations = this._getDecorationsInRange(range, ownerId, filterOutValidation, filterFontDecorations, onlyMarginDecorations); - pushMany(decorations, this._decorationProvider.getDecorationsInRange(range, ownerId, filterOutValidation)); - pushMany(decorations, this._fontTokenDecorationsProvider.getDecorationsInRange(range, ownerId, filterOutValidation)); + pushMany(decorations, this._decorationProvider.getDecorationsInRange(range, ownerId, filterOutValidation, filterFontDecorations)); + pushMany(decorations, this._fontTokenDecorationsProvider.getDecorationsInRange(range, ownerId, filterOutValidation, filterFontDecorations)); return decorations; } @@ -1828,8 +1828,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const validatedRange = this.validateRange(range); const decorations = this._getDecorationsInRange(validatedRange, ownerId, filterOutValidation, filterFontDecorations, onlyMarginDecorations); - pushMany(decorations, this._decorationProvider.getDecorationsInRange(validatedRange, ownerId, filterOutValidation, onlyMinimapDecorations)); - pushMany(decorations, this._fontTokenDecorationsProvider.getDecorationsInRange(validatedRange, ownerId, filterOutValidation, onlyMinimapDecorations)); + pushMany(decorations, this._decorationProvider.getDecorationsInRange(validatedRange, ownerId, filterOutValidation, filterFontDecorations, onlyMinimapDecorations)); + pushMany(decorations, this._fontTokenDecorationsProvider.getDecorationsInRange(validatedRange, ownerId, filterOutValidation, filterFontDecorations, onlyMinimapDecorations)); return decorations; } diff --git a/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts b/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts index 0481e1507fc92..ef7e2d3bfb426 100644 --- a/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts +++ b/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts @@ -118,31 +118,34 @@ export class TokenizationFontDecorationProvider extends Disposable implements De this._onDidChangeFont.fire(affectedLineFonts); } - public getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[] { + public getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean, filterFontDecorations?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[] { const startOffsetOfRange = this.textModel.getOffsetAt(range.getStartPosition()); const endOffsetOfRange = this.textModel.getOffsetAt(range.getEndPosition()); const annotations = this._fontAnnotatedString.getAnnotationsIntersecting(new OffsetRange(startOffsetOfRange, endOffsetOfRange)); const decorations: IModelDecoration[] = []; for (const annotation of annotations) { - const annotationStartPosition = this.textModel.getPositionAt(annotation.range.start); - const annotationEndPosition = this.textModel.getPositionAt(annotation.range.endExclusive); - const range = Range.fromPositions(annotationStartPosition, annotationEndPosition); const anno = annotation.annotation; - const className = classNameForFontTokenDecorations(anno.fontToken.fontFamily ?? '', anno.fontToken.fontSizeMultiplier ?? 0); const affectsFont = !!(anno.fontToken.fontFamily || anno.fontToken.fontSizeMultiplier); - const id = anno.decorationId; - decorations.push({ - id: id, - options: { - description: 'FontOptionDecoration', - inlineClassName: className, - lineHeight: anno.fontToken.lineHeightMultiplier, - affectsFont - }, - ownerId: 0, - range - }); + if (!(affectsFont && filterFontDecorations)) { + const annotationStartPosition = this.textModel.getPositionAt(annotation.range.start); + const annotationEndPosition = this.textModel.getPositionAt(annotation.range.endExclusive); + const range = Range.fromPositions(annotationStartPosition, annotationEndPosition); + const anno = annotation.annotation; + const className = classNameForFontTokenDecorations(anno.fontToken.fontFamily ?? '', anno.fontToken.fontSizeMultiplier ?? 0); + const id = anno.decorationId; + decorations.push({ + id: id, + options: { + description: 'FontOptionDecoration', + inlineClassName: className, + lineHeight: anno.fontToken.lineHeightMultiplier, + affectsFont + }, + ownerId: 0, + range + }); + } } return decorations; } diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 7d48eaa295e2e..a33540a913d4d 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -9,7 +9,7 @@ import { IListAccessibilityProvider, List } from '../../../base/browser/ui/list/ import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; import { Codicon } from '../../../base/common/codicons.js'; import { ResolvedKeybinding } from '../../../base/common/keybindings.js'; -import { Disposable } from '../../../base/common/lifecycle.js'; +import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; import { OS } from '../../../base/common/platform.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import './actionWidget.css'; @@ -19,6 +19,10 @@ import { IKeybindingService } from '../../keybinding/common/keybinding.js'; import { defaultListStyles } from '../../theme/browser/defaultStyles.js'; import { asCssVariable } from '../../theme/common/colorRegistry.js'; import { ILayoutService } from '../../layout/browser/layoutService.js'; +import { IHoverService } from '../../hover/browser/hover.js'; +import { MarkdownString } from '../../../base/common/htmlContent.js'; +import { HoverPosition } from '../../../base/browser/ui/hover/hoverWidget.js'; +import { IHoverWidget } from '../../../base/browser/ui/hover/hover.js'; export const acceptSelectedActionCommand = 'acceptSelectedCodeAction'; export const previewSelectedActionCommand = 'previewSelectedCodeAction'; @@ -30,6 +34,16 @@ export interface IActionListDelegate { onFocus?(action: T | undefined): void; } +/** + * Optional hover configuration shown when focusing/hovering over an action list item. + */ +export interface IActionListItemHover { + /** + * Content to display in the hover. + */ + readonly content?: string; +} + export interface IActionListItem { readonly item?: T; readonly kind: ActionListItemKind; @@ -37,6 +51,10 @@ export interface IActionListItem { readonly disabled?: boolean; readonly label?: string; readonly description?: string; + /** + * Optional hover configuration shown when focusing/hovering over the item. + */ + readonly hover?: IActionListItemHover; readonly keybinding?: ResolvedKeybinding; canPreview?: boolean | undefined; readonly hideIcon?: boolean; @@ -179,6 +197,9 @@ class ActionItemRenderer implements IListRenderer, IAction data.container.title = element.tooltip; } else if (element.disabled) { data.container.title = element.label; + } else if (element.hover?.content) { + // Don't show tooltip when hover content is configured - the rich hover will show instead + data.container.title = ''; } else if (actionTitle && previewTitle) { if (this._supportsPreview && element.canPreview) { data.container.title = localize({ key: 'label-preview', comment: ['placeholders are keybindings, e.g "F2 to Apply, Shift+F2 to Preview"'] }, "{0} to Apply, {1} to Preview", actionTitle, previewTitle); @@ -225,6 +246,8 @@ export class ActionList extends Disposable { private readonly cts = this._register(new CancellationTokenSource()); + private hover: { index: number; hover: IHoverWidget } | undefined; + constructor( user: string, preview: boolean, @@ -234,6 +257,7 @@ export class ActionList extends Disposable { @IContextViewService private readonly _contextViewService: IContextViewService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @ILayoutService private readonly _layoutService: ILayoutService, + @IHoverService private readonly _hoverService: IHoverService, ) { super(); this.domNode = document.createElement('div'); @@ -298,6 +322,9 @@ export class ActionList extends Disposable { this._register(this._list.onDidChangeFocus(() => this.onFocus())); this._register(this._list.onDidChangeSelection(e => this.onListSelection(e))); + // Ensure hover is hidden when ActionList is disposed + this._register(toDisposable(() => this.hideHover())); + this._allMenuItems = items; this._list.splice(0, this._list.length, this._allMenuItems); @@ -313,6 +340,7 @@ export class ActionList extends Disposable { hide(didCancel?: boolean): void { this._delegate.onHide(didCancel); this.cts.cancel(); + this.hideHover(); this._contextViewService.hideContextView(); } @@ -331,8 +359,7 @@ export class ActionList extends Disposable { } else { // For finding width dynamically (not using resize observer) const itemWidths: number[] = this._allMenuItems.map((_, index): number => { - // eslint-disable-next-line no-restricted-syntax - const element = this.domNode.ownerDocument.getElementById(this._list.getElementID(index)); + const element = this._getRowElement(index); if (element) { element.style.width = 'auto'; const width = element.getBoundingClientRect().width; @@ -393,6 +420,15 @@ export class ActionList extends Disposable { } } + private hideHover() { + if (this.hover) { + if (!this.hover.hover.isDisposed) { + this.hover.hover.dispose(); + } + this.hover = undefined; + } + } + private onFocus() { const focused = this._list.getFocus(); if (focused.length === 0) { @@ -401,10 +437,52 @@ export class ActionList extends Disposable { const focusIndex = focused[0]; const element = this._list.element(focusIndex); this._delegate.onFocus?.(element.item); + + // Show hover on focus change + this._showHoverForElement(element, focusIndex); + } + + private _getRowElement(index: number): HTMLElement | null { + // eslint-disable-next-line no-restricted-syntax + return this.domNode.ownerDocument.getElementById(this._list.getElementID(index)); + } + + private _showHoverForElement(element: IActionListItem, index: number): void { + // Hide any existing hover when moving to a different item + if (this.hover) { + if (this.hover.index === index && !this.hover.hover.isDisposed) { + return; + } + this.hideHover(); + } + + // Show hover if the element has hover content + if (element.hover?.content && this.focusCondition(element)) { + // The List widget separates data models from DOM elements, so we need to + // look up the actual DOM node to use as the hover target. + const rowElement = this._getRowElement(index); + if (rowElement) { + const markdown = element.hover.content ? new MarkdownString(element.hover.content) : undefined; + const hover = this._hoverService.showInstantHover({ + content: markdown ?? '', + target: rowElement, + additionalClasses: ['action-widget-hover'], + position: { + hoverPosition: HoverPosition.LEFT, + forcePosition: false, + }, + appearance: { + showPointer: true, + }, + }); + this.hover = hover ? { index, hover } : undefined; + } + } } private async onListHover(e: IListMouseEvent>) { const element = e.element; + if (element && element.item && this.focusCondition(element)) { if (this._delegate.onHover && !element.disabled && element.kind === ActionListItemKind.Action) { const result = await this._delegate.onHover(element.item, this.cts.token); @@ -413,9 +491,9 @@ export class ActionList extends Disposable { if (e.index) { this._list.splice(e.index, 1, [element]); } - } - this._list.setFocus(typeof e.index === 'number' ? [e.index] : []); + this._list.setFocus(typeof e.index === 'number' ? [e.index] : []); + } } private onListClick(e: IListMouseEvent>): void { diff --git a/src/vs/platform/actionWidget/browser/actionWidget.ts b/src/vs/platform/actionWidget/browser/actionWidget.ts index 5ab460e6790f3..21b49245bebcc 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.ts +++ b/src/vs/platform/actionWidget/browser/actionWidget.ts @@ -141,7 +141,14 @@ class ActionWidgetService extends Disposable implements IActionWidgetService { widget.style.width = `${width}px`; const focusTracker = renderDisposables.add(dom.trackFocus(element)); - renderDisposables.add(focusTracker.onDidBlur(() => this.hide(true))); + renderDisposables.add(focusTracker.onDidBlur(() => { + // Don't hide if focus moved to a hover that belongs to this action widget + const activeElement = dom.getActiveElement(); + if (activeElement?.closest('.action-widget-hover')) { + return; + } + this.hide(true); + })); return renderDisposables; } diff --git a/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts index 2b5897deff6b2..2021c617ce47e 100644 --- a/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts +++ b/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts @@ -6,7 +6,7 @@ import { IActionWidgetService } from './actionWidget.js'; import { IAction } from '../../../base/common/actions.js'; import { BaseDropdown, IActionProvider, IBaseDropdownOptions } from '../../../base/browser/ui/dropdown/dropdown.js'; -import { ActionListItemKind, IActionListDelegate, IActionListItem } from './actionList.js'; +import { ActionListItemKind, IActionListDelegate, IActionListItem, IActionListItemHover } from './actionList.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { Codicon } from '../../../base/common/codicons.js'; import { getActiveElement, isHTMLElement } from '../../../base/browser/dom.js'; @@ -17,6 +17,10 @@ export interface IActionWidgetDropdownAction extends IAction { category?: { label: string; order: number; showHeader?: boolean }; icon?: ThemeIcon; description?: string; + /** + * Optional flyout hover configuration shown when focusing/hovering over the action. + */ + hover?: IActionListItemHover; } // TODO @lramos15 - Should we just make IActionProvider templated? @@ -103,6 +107,7 @@ export class ActionWidgetDropdown extends BaseDropdown { item: action, tooltip: action.tooltip, description: action.description, + hover: action.hover, kind: ActionListItemKind.Action, canPreview: false, group: { title: '', icon: action.icon ?? ThemeIcon.fromId(action.checked ? Codicon.check.id : Codicon.blank.id) }, diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 5a424bb9ff009..5695a7125d2dd 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -66,6 +66,12 @@ export async function collectWorkspaceStats(folder: string, filter: string[]): P { tag: 'agent.md', filePattern: /^agent\.md$/i }, { tag: 'agents.md', filePattern: /^agents\.md$/i }, { tag: 'claude.md', filePattern: /^claude\.md$/i }, + { tag: 'claude-settings', filePattern: /^settings\.json$/i, relativePathPattern: /^\.claude$/i }, + { tag: 'claude-settings-local', filePattern: /^settings\.local\.json$/i, relativePathPattern: /^\.claude$/i }, + { tag: 'claude-mcp', filePattern: /^mcp\.json$/i, relativePathPattern: /^\.claude$/i }, + { tag: 'claude-commands-dir', filePattern: /\.md$/i, relativePathPattern: /^\.claude[\/\\]commands$/i }, + { tag: 'claude-skills-dir', filePattern: /^SKILL\.md$/i, relativePathPattern: /^\.claude[\/\\]skills[\/\\]/i }, + { tag: 'claude-rules-dir', filePattern: /\.md$/i, relativePathPattern: /^\.claude[\/\\]rules$/i }, { tag: 'gemini.md', filePattern: /^gemini\.md$/i }, { tag: 'copilot-instructions.md', filePattern: /^copilot\-instructions\.md$/i, relativePathPattern: /^\.github$/i }, ]; diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index 1fda3db0826ba..14f5cb830f774 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -297,7 +297,7 @@ class ToggleScreencastModeAction extends Action2 { keyboardMarker.innerText = ''; append(keyboardMarker, $('span.key', {}, `Backspace`)); } - clearKeyboardScheduler.schedule(); + clearKeyboardScheduler.schedule(keyboardMarkerTimeout); })); disposables.add(onCompositionEnd.event(e => { @@ -315,7 +315,7 @@ class ToggleScreencastModeAction extends Action2 { } else { imeBackSpace = true; } - clearKeyboardScheduler.schedule(); + clearKeyboardScheduler.schedule(keyboardMarkerTimeout); return; } @@ -381,7 +381,7 @@ class ToggleScreencastModeAction extends Action2 { } length++; - clearKeyboardScheduler.schedule(); + clearKeyboardScheduler.schedule(keyboardMarkerTimeout); })); ToggleScreencastModeAction.disposable = disposables; diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts index ccececc4b40c7..3604b8b75eb5d 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts @@ -5,7 +5,7 @@ import './media/browser.css'; import { localize } from '../../../../nls.js'; -import { $, addDisposableListener, disposableWindowInterval, EventType, isHTMLElement, registerExternalFocusChecker, scheduleAtNextAnimationFrame } from '../../../../base/browser/dom.js'; +import { $, addDisposableListener, Dimension, disposableWindowInterval, EventType, IDomPosition, isHTMLElement, registerExternalFocusChecker } 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'; @@ -29,7 +29,7 @@ import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js import { BrowserOverlayManager } from './overlayManager.js'; import { getZoomFactor, onDidChangeZoomLevel } from '../../../../base/browser/browser.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { WorkbenchHoverDelegate } from '../../../../platform/hover/browser/hover.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; @@ -250,6 +250,11 @@ export class BrowserEditor extends EditorPane { // Register external focus checker so that cross-window focus logic knows when // this browser view has focus (since it's outside the normal DOM tree). this._register(registerExternalFocusChecker(() => this._model?.focused ?? false)); + + // Automatically call layoutBrowserContainer() when the browser container changes size + const resizeObserver = new ResizeObserver(async () => this.layoutBrowserContainer()); + resizeObserver.observe(this._browserContainer); + this._register(toDisposable(() => resizeObserver.disconnect())); } override async setInput(input: BrowserEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { @@ -354,7 +359,7 @@ export class BrowserEditor extends EditorPane { // Listen for zoom level changes and update browser view zoom factor this._inputDisposables.add(onDidChangeZoomLevel(targetWindowId => { if (targetWindowId === this.window.vscodeWindowId) { - this.layout(); + this.layoutBrowserContainer(); } })); // Capture screenshot periodically (once per second) to keep background updated @@ -365,11 +370,8 @@ export class BrowserEditor extends EditorPane { )); this.updateErrorDisplay(); - this.layout(); + this.layoutBrowserContainer(); await this._model.setVisible(this.shouldShowView); - - // Sometimes the element has not been inserted into the DOM yet. Ensure layout after next animation frame. - scheduleAtNextAnimationFrame(this.window, () => this.layout()); } protected override setEditorVisible(visible: boolean): void { @@ -675,7 +677,20 @@ export class BrowserEditor extends EditorPane { } } - override layout(): void { + override layout(_dimension: Dimension, _position?: IDomPosition): void { + // no-op: layout is handled in layoutBrowserContainer() + } + + /** + * This should be called whenever .browser-container changes in size, or when + * there could be any elements, such as the command palette, overlapping with it. + * + * Note that we don't call layoutBrowserContainer() from layout() but instead rely on using a ResizeObserver and on + * making direct calls to it. This is because we have seen cases where the getBoundingClientRect() values of + * the .browser-container element are not correct during layout() calls, especially during "Move into New Window" + * and "Copy into New Window" operations into a different monitor. + */ + layoutBrowserContainer(): void { if (this._model) { this.checkOverlays(); diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserEditorInput.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserEditorInput.ts index 57d42830dd2ae..bc23c8a501dff 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserEditorInput.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserEditorInput.ts @@ -7,6 +7,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 { generateUuid } from '../../../../base/common/uuid.js'; import { BrowserViewUri } from '../../../../platform/browserView/common/browserViewUri.js'; import { EditorInputCapabilities, IEditorSerializer, IUntypedEditorInput } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; @@ -56,7 +57,8 @@ export class BrowserEditorInput extends EditorInput { options: IBrowserEditorInputData, @IThemeService private readonly themeService: IThemeService, @IBrowserViewWorkbenchService private readonly browserViewWorkbenchService: IBrowserViewWorkbenchService, - @ILifecycleService private readonly lifecycleService: ILifecycleService + @ILifecycleService private readonly lifecycleService: ILifecycleService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); this._id = options.id; @@ -205,6 +207,20 @@ export class BrowserEditorInput extends EditorInput { return false; } + /** + * Creates a copy of this browser editor input with a new unique ID, creating an independent browser view with no linked state. + * This is used during Copy into New Window. + */ + override copy(): EditorInput { + const currentUrl = this._model?.url ?? this._initialData.url; + return this.instantiationService.createInstance(BrowserEditorInput, { + id: generateUuid(), + url: currentUrl, + title: this._model?.title ?? this._initialData.title, + favicon: this._model?.favicon ?? this._initialData.favicon + }); + } + override toUntyped(): IUntypedEditorInput { return { resource: this.resource, diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts index 648d7fd1329c8..cf389342c9380 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts @@ -21,7 +21,7 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { IChatEditingSession } from '../../common/editing/chatEditingService.js'; import { IChatService } from '../../common/chatService/chatService.js'; import { localChatSessionType } from '../../common/chatSessionsService.js'; -import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; import { getChatSessionType, LocalChatSessionUri } from '../../common/model/chatUri.js'; import { ChatViewId, IChatWidgetService, isIChatViewViewContext } from '../chat.js'; import { EditingSessionAction, EditingSessionActionContext, getEditingSessionContext } from '../chatEditing/chatEditingActions.js'; @@ -30,6 +30,7 @@ import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; import { ACTION_ID_NEW_CHAT, ACTION_ID_NEW_EDIT_SESSION, CHAT_CATEGORY, handleCurrentEditingSession } from './chatActions.js'; import { clearChatEditor } from './chatClear.js'; import { AgentSessionProviders, AgentSessionsViewerOrientation } from '../agentSessions/agentSessions.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; export interface INewEditSessionActionContext { @@ -294,6 +295,7 @@ async function runNewChatAction( ) { const accessibilityService = accessor.get(IAccessibilityService); const viewsService = accessor.get(IViewsService); + const configurationService = accessor.get(IConfigurationService); const { editingSession, chatWidget: widget } = context ?? {}; if (!widget) { @@ -334,6 +336,8 @@ async function runNewChatAction( if (typeof executeCommandContext.agentMode === 'boolean') { widget.input.setChatMode(executeCommandContext.agentMode ? ChatModeKind.Agent : ChatModeKind.Edit); + } else if (widget.input.currentModeKind === ChatModeKind.Edit && configurationService.getValue(ChatConfiguration.EditModeHidden)) { + widget.input.setChatMode(ChatModeKind.Agent); } if (executeCommandContext.inputValue) { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts index 29b7f2d871495..fad1044c603bf 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsModel.ts @@ -14,10 +14,13 @@ import { ResourceMap } from '../../../../../base/common/map.js'; import { MarshalledId } from '../../../../../base/common/marshallingIds.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI, UriComponents } from '../../../../../base/common/uri.js'; +import { localize } from '../../../../../nls.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; +import { ILogService, LogLevel } from '../../../../../platform/log/common/log.js'; +import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; +import { Extensions, IOutputChannelRegistry, IOutputService } from '../../../../services/output/common/output.js'; import { ChatSessionStatus as AgentSessionStatus, IChatSessionFileChange, IChatSessionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus } from '../../common/chatSessionsService.js'; import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName } from './agentSessions.js'; @@ -181,6 +184,195 @@ export function isMarshalledAgentSessionContext(thing: unknown): thing is IMarsh //#endregion +//#region Sessions Logger + +const agentSessionsOutputChannelId = 'agentSessionsOutput'; +const agentSessionsOutputChannelLabel = localize('agentSessionsOutput', "Agent Sessions"); + +function statusToString(status: AgentSessionStatus): string { + switch (status) { + case AgentSessionStatus.Failed: return 'Failed'; + case AgentSessionStatus.Completed: return 'Completed'; + case AgentSessionStatus.InProgress: return 'InProgress'; + case AgentSessionStatus.NeedsInput: return 'NeedsInput'; + default: return `Unknown(${status})`; + } +} + +interface ISessionToStateEntry { + status: AgentSessionStatus; + inProgressTime?: number; + finishedOrFailedTime?: number; +} + +class AgentSessionsLogger extends Disposable { + + constructor( + private readonly getSessionsData: () => { + sessions: Iterable; + sessionStates: ResourceMap; + mapSessionToState: ResourceMap; + }, + @ILogService private readonly logService: ILogService, + @IOutputService private readonly outputService: IOutputService, + ) { + super(); + + this.registerOutputChannel(); + this.registerListeners(); + } + + private registerOutputChannel(): void { + Registry.as(Extensions.OutputChannels).registerChannel({ + id: agentSessionsOutputChannelId, + label: agentSessionsOutputChannelLabel, + log: false + }); + } + + private registerListeners(): void { + this._register(this.logService.onDidChangeLogLevel(level => { + if (level === LogLevel.Trace) { + this.logIfTrace('Log level changed to trace'); + } + })); + } + + logIfTrace(reason: string): void { + if (this.logService.getLevel() !== LogLevel.Trace) { + return; + } + + this.logAllSessions(reason); + this.logSessionStates(); + this.logMapSessionToState(); + } + + private logAllSessions(reason: string): void { + const channel = this.outputService.getChannel(agentSessionsOutputChannelId); + if (!channel) { + return; + } + + const { sessions, sessionStates } = this.getSessionsData(); + + const lines: string[] = []; + lines.push(`=== Agent Sessions (${reason}) ===`); + + let count = 0; + for (const session of sessions) { + count++; + const state = sessionStates.get(session.resource); + + lines.push(`--- Session: ${session.label} ---`); + lines.push(` Resource: ${session.resource.toString()}`); + lines.push(` Provider Type: ${session.providerType}`); + lines.push(` Provider Label: ${session.providerLabel}`); + lines.push(` Status: ${statusToString(session.status)}`); + lines.push(` Icon: ${session.icon.id}`); + + if (session.description) { + lines.push(` Description: ${typeof session.description === 'string' ? session.description : session.description.value}`); + } + if (session.badge) { + lines.push(` Badge: ${typeof session.badge === 'string' ? session.badge : session.badge.value}`); + } + if (session.tooltip) { + lines.push(` Tooltip: ${typeof session.tooltip === 'string' ? session.tooltip : session.tooltip.value}`); + } + + // Timing info + lines.push(` Timing:`); + lines.push(` Created: ${session.timing.created ? new Date(session.timing.created).toISOString() : 'N/A'}`); + lines.push(` Last Request Started: ${session.timing.lastRequestStarted ? new Date(session.timing.lastRequestStarted).toISOString() : 'N/A'}`); + lines.push(` Last Request Ended: ${session.timing.lastRequestEnded ? new Date(session.timing.lastRequestEnded).toISOString() : 'N/A'}`); + if (session.timing.inProgressTime) { + lines.push(` In Progress Time: ${new Date(session.timing.inProgressTime).toISOString()}`); + } + if (session.timing.finishedOrFailedTime) { + lines.push(` Finished/Failed Time: ${new Date(session.timing.finishedOrFailedTime).toISOString()}`); + } + + // Changes info + if (session.changes) { + const summary = getAgentChangesSummary(session.changes); + if (summary) { + lines.push(` Changes: ${summary.files} files, +${summary.insertions} -${summary.deletions}`); + } + } + + // Our state (read/unread, archived) + lines.push(` State:`); + lines.push(` Archived (provider): ${session.archived ?? 'N/A'}`); + lines.push(` Archived (computed): ${session.isArchived()}`); + lines.push(` Archived (stored): ${state?.archived ?? 'N/A'}`); + lines.push(` Read: ${session.isRead()}`); + lines.push(` Read date (stored): ${state?.read ? new Date(state.read).toISOString() : 'N/A'}`); + + lines.push(''); + } + + lines.unshift(`Total sessions: ${count}`, ''); + + lines.push(`=== End Agent Sessions ===`); + + channel.append(lines.join('\n') + '\n'); + } + + private logSessionStates(): void { + const channel = this.outputService.getChannel(agentSessionsOutputChannelId); + if (!channel) { + return; + } + + const { sessionStates } = this.getSessionsData(); + + const lines: string[] = []; + lines.push(`=== Session States ===`); + lines.push(`Total stored states: ${sessionStates.size}`); + lines.push(''); + + for (const [resource, state] of sessionStates) { + lines.push(`URI: ${resource.toString()}`); + lines.push(` Archived: ${state.archived}`); + lines.push(` Read: ${state.read ? new Date(state.read).toISOString() : '0 (unread)'}`); + lines.push(''); + } + + lines.push(`=== End Session States ===`); + + channel.append(lines.join('\n') + '\n'); + } + + private logMapSessionToState(): void { + const channel = this.outputService.getChannel(agentSessionsOutputChannelId); + if (!channel) { + return; + } + + const { mapSessionToState } = this.getSessionsData(); + + const lines: string[] = []; + lines.push(`=== Map Session To State (Status Tracking) ===`); + lines.push(`Total entries: ${mapSessionToState.size}`); + lines.push(''); + + for (const [resource, state] of mapSessionToState) { + lines.push(`URI: ${resource.toString()}`); + lines.push(` Status: ${statusToString(state.status)}`); + lines.push(` In Progress Time: ${state.inProgressTime ? new Date(state.inProgressTime).toISOString() : 'N/A'}`); + lines.push(` Finished/Failed Time: ${state.finishedOrFailedTime ? new Date(state.finishedOrFailedTime).toISOString() : 'N/A'}`); + lines.push(''); + } + + lines.push(`=== End Map Session To State ===`); + + channel.append(lines.join('\n') + '\n'); + } +} + +//#endregion + export class AgentSessionsModel extends Disposable implements IAgentSessionsModel { private readonly _onWillResolve = this._register(new Emitter()); @@ -206,13 +398,13 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode }>(); private readonly cache: AgentSessionsCache; + private readonly logger: AgentSessionsLogger; constructor( @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService private readonly storageService: IStorageService, - @ILogService private readonly logService: ILogService, ) { super(); @@ -225,7 +417,18 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } this.sessionStates = this.cache.loadSessionStates(); + this.logger = this._register(this.instantiationService.createInstance( + AgentSessionsLogger, + () => ({ + sessions: this._sessions.values(), + sessionStates: this.sessionStates, + mapSessionToState: this.mapSessionToState + }) + )); + this.logger.logIfTrace('Loaded cached sessions'); + this.registerListeners(); + } private registerListeners(): void { @@ -273,8 +476,6 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode const providersToResolve = Array.from(this.providersToResolve); this.providersToResolve.clear(); - this.logService.trace(`[agent sessions] Resolving agent sessions for providers: ${providersToResolve.map(p => p ?? 'all').join(', ')}`); - const mapSessionContributionToType = new Map(); for (const contribution of this.chatSessionsService.getAllChatSessionContributions()) { mapSessionContributionToType.set(contribution.type, contribution); @@ -290,8 +491,6 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode const sessions = new ResourceMap(); for (const { chatSessionType, items: providerSessions } of providerResults) { - this.logService.trace(`[agent sessions] Resolved ${providerSessions.length} agent sessions for provider ${chatSessionType}`); - resolvedProviders.add(chatSessionType); if (token.isCancellationRequested) { @@ -398,7 +597,6 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } this._sessions = sessions; - this.logService.trace(`[agent sessions] Total resolved agent sessions:`, Array.from(this._sessions.values())); for (const [resource] of this.mapSessionToState) { if (!sessions.has(resource)) { @@ -412,6 +610,8 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode } } + this.logger.logIfTrace('Sessions resolved from providers'); + this._onDidChangeSessions.fire(); } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts index 50a3b94aea5bc..481f4bed2f104 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewer.ts @@ -192,6 +192,7 @@ export class AgentSessionRenderer extends Disposable implements ICompressibleTre } } template.diffContainer.classList.toggle('has-diff', hasDiff); + ChatContextKeys.hasAgentSessionChanges.bindTo(template.contextKeyService).set(hasDiff); // Badge let hasBadge = false; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css index 190ef9eceb194..846c2ff8fd906 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css @@ -59,7 +59,7 @@ .monaco-list-row:hover .agent-session-title-toolbar, .monaco-list-row.focused .agent-session-title-toolbar { - width: 22px; + width: 44px; .monaco-toolbar { display: block; diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 7dfeb8d00443a..7f2c4473ab125 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -567,6 +567,12 @@ configurationRegistry.registerConfiguration({ } } }, + [ChatConfiguration.EditModeHidden]: { + type: 'boolean', + description: nls.localize('chat.editMode.hidden', "When enabled, hides the Edit mode from the chat mode picker."), + default: false, + tags: ['experimental'], + }, [ChatConfiguration.EnableMath]: { type: 'boolean', description: nls.localize('chat.mathEnabled.description', "Enable math rendering in chat responses using KaTeX."), diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 30404affd13ea..5d245b48a1c69 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -35,6 +35,7 @@ import { isChatTreeItem, isRequestVM, isResponseVM } from '../../common/model/ch import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; import { CHAT_CATEGORY } from '../actions/chatActions.js'; import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js'; +import { IAgentSession, isAgentSession } from '../agentSessions/agentSessionsModel.js'; export abstract class EditingSessionAction extends Action2 { @@ -323,18 +324,29 @@ export class ViewAllSessionChangesAction extends Action2 { group: 'navigation', order: 10, when: ChatContextKeys.hasAgentSessionChanges + }, + { + id: MenuId.AgentSessionItemToolbar, + group: 'navigation', + order: 0, + when: ChatContextKeys.hasAgentSessionChanges } ], }); } - override async run(accessor: ServicesAccessor, sessionResource?: URI): Promise { + override async run(accessor: ServicesAccessor, sessionOrSessionResource?: URI | IAgentSession): Promise { const agentSessionsService = accessor.get(IAgentSessionsService); const commandService = accessor.get(ICommandService); - if (!URI.isUri(sessionResource)) { + + if (!URI.isUri(sessionOrSessionResource) && !isAgentSession(sessionOrSessionResource)) { return; } + const sessionResource = URI.isUri(sessionOrSessionResource) + ? sessionOrSessionResource + : sessionOrSessionResource.resource; + const session = agentSessionsService.getSession(sessionResource); const changes = session?.changes; if (!(changes instanceof Array)) { diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagementEditor.ts index 36355c7870bf7..1226a59d0f675 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatManagementEditor.ts @@ -94,6 +94,10 @@ export class ModelsManagementEditor extends EditorPane { clearSearch(): void { this.modelsWidget?.clearSearch(); } + + search(query: string): void { + this.modelsWidget?.search(query); + } } export const chatManagementSashBorder = registerColor('chatManagement.sashBorder', PANEL_BORDER, localize('chatManagementSashBorder', "The color of the Chat Management editor splitview sash border.")); diff --git a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts index 1dc5e58c8ebda..23d1cd4a26c66 100644 --- a/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsWidget.ts @@ -993,6 +993,7 @@ export class ChatModelsWidget extends Disposable { this.createTable(); this._register(this.viewModel.onDidChangeGrouping(() => this.createTable())); this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.updateAddModelsButton())); + this._register(this.languageModelsService.onDidChangeLanguageModelVendors(() => this.updateAddModelsButton())); } private createTable(): void { @@ -1168,7 +1169,6 @@ export class ChatModelsWidget extends Disposable { this.table.setFocus([selectedEntryIndex]); this.table.setSelection([selectedEntryIndex]); } - this.updateAddModelsButton(); })); this.tableDisposables.add(this.table.onDidOpen(async ({ element, browserEvent }) => { @@ -1205,6 +1205,7 @@ export class ChatModelsWidget extends Disposable { && entitlement !== ChatEntitlement.Available && entitlement !== ChatEntitlement.Business && entitlement !== ChatEntitlement.Enterprise); + this.addButton.enabled = supportsAddingModels && configurableVendors.length > 0; this.dropdownActions = configurableVendors.map(vendor => toAction({ diff --git a/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts b/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts index cde93d3d618fc..3424791911d18 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelsConfigurationService.ts @@ -282,13 +282,11 @@ export class ChatLanguageModelsDataContribution extends Disposable implements IW constructor( @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, - @IUserDataProfileService userDataProfileService: IUserDataProfileService, - @IUriIdentityService uriIdentityService: IUriIdentityService, + @ILanguageModelsConfigurationService languageModelsConfigurationService: ILanguageModelsConfigurationService, ) { super(); - const modelsConfigurationFile = uriIdentityService.extUri.joinPath(userDataProfileService.currentProfile.location, 'models.json'); const registry = Registry.as(JSONExtensions.JSONContribution); - this._register(registry.registerSchemaAssociation(languageModelsSchemaId, modelsConfigurationFile.toString())); + this._register(registry.registerSchemaAssociation(languageModelsSchemaId, languageModelsConfigurationService.configurationFile.toString())); this.updateSchema(registry); this._register(this.languageModelsService.onDidChangeLanguageModels(() => this.updateSchema(registry))); diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index a37156c6a6234..2d29e9c5c9e5f 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -43,6 +43,8 @@ import { ChatConfiguration } from '../../common/constants.js'; import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.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'; +import { URI } from '../../../../../base/common/uri.js'; +import { chatSessionResourceToId } from '../../common/model/chatUri.js'; const jsonSchemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); @@ -83,7 +85,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private readonly _onDidChangeTools = this._register(new Emitter()); readonly onDidChangeTools = this._onDidChangeTools.event; - private readonly _onDidPrepareToolCallBecomeUnresponsive = this._register(new Emitter<{ sessionId: string; toolData: IToolData }>()); + private readonly _onDidPrepareToolCallBecomeUnresponsive = this._register(new Emitter<{ sessionResource: URI; toolData: IToolData }>()); readonly onDidPrepareToolCallBecomeUnresponsive = this._onDidPrepareToolCallBecomeUnresponsive.event; /** Throttle tools updates because it sends all tools and runs on context key updates */ @@ -489,7 +491,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo 'languageModelToolInvoked', { result: 'success', - chatSessionId: dto.context?.sessionId, + chatSessionId: dto.context?.sessionResource ? chatSessionResourceToId(dto.context.sessionResource) : undefined, toolId: tool.data.id, toolExtensionId: tool.data.source.type === 'extension' ? tool.data.source.extensionId.value : undefined, toolSourceKind: tool.data.source.type, @@ -542,9 +544,9 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo timeout(3000, token).then(() => 'timeout'), preparePromise ]); - if (raceResult === 'timeout') { + if (raceResult === 'timeout' && dto.context) { this._onDidPrepareToolCallBecomeUnresponsive.fire({ - sessionId: dto.context?.sessionId ?? '', + sessionResource: dto.context.sessionResource, toolData: tool.data }); } 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 e9436e9ad6568..f13918be0b967 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts @@ -25,6 +25,7 @@ import { AccessibilityWorkbenchSettingId } from '../../../../accessibility/brows import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { HoverStyle } from '../../../../../../base/browser/ui/hover/hover.js'; import { ILanguageModelToolsService } from '../../../common/tools/languageModelToolsService.js'; +import { isEqual } from '../../../../../../base/common/resources.js'; export class ChatProgressContentPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; @@ -165,7 +166,7 @@ export class ChatWorkingProgressContentPart extends ChatProgressContentPart impl }; super(progressMessage, chatContentMarkdownRenderer, context, undefined, undefined, undefined, undefined, instantiationService, chatMarkdownAnchorService, configurationService); this._register(languageModelToolsService.onDidPrepareToolCallBecomeUnresponsive(e => { - if (context.element.sessionId === e.sessionId) { + if (isEqual(context.element.sessionResource, e.sessionResource)) { this.updateMessage(new MarkdownString(localize('toolCallUnresponsive', "Waiting for tool '{0}' to respond...", e.toolData.displayName))); } })); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts index 98545c506cdef..a1414f7cf7957 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts @@ -116,10 +116,6 @@ export class ChatMcpAppSubPart extends BaseChatToolInvocationSubPart { this._model.remount(); } })); - - this._register(onDidRemount(() => { - this._model.remount(); - })); } private _handleLoadStateChange(container: HTMLElement, loadState: McpAppLoadState): void { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 642cc3bea386d..482687545f5c1 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -584,7 +584,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } get contentHeight(): number { - return this.input.inputPartHeight.get() + this.listWidget.contentHeight + this.chatSuggestNextWidget.height; + return this.input.height.get() + this.listWidget.contentHeight + this.chatSuggestNextWidget.height; } get attachmentModel(): ChatAttachmentModel { @@ -1652,6 +1652,18 @@ export class ChatWidget extends Disposable implements IChatWidget { this.styles, true ); + this._register(autorun(reader => { + this.inlineInputPart.height.read(reader); + if (!this.listWidget) { + // This is set up before the list/renderer are created + return; + } + + const editedRequest = this.listWidget.getTemplateDataForRequestId(this.viewModel?.editing?.id); + if (isRequestVM(editedRequest?.currentElement) && this.viewModel?.editing) { + this.listWidget.updateItemHeightOnRender(editedRequest?.currentElement, editedRequest); + } + })); } else { this.inputPartDisposable.value = this.instantiationService.createInstance(ChatInputPart, this.location, @@ -1659,6 +1671,19 @@ export class ChatWidget extends Disposable implements IChatWidget { this.styles, false ); + this._register(autorun(reader => { + this.inputPart.height.read(reader); + if (!this.listWidget) { + // This is set up before the list/renderer are created + return; + } + + if (this.bodyDimension) { + this.layout(this.bodyDimension.height, this.bodyDimension.width); + } + + this._onDidChangeContentHeight.fire(); + })); } this.input.render(container, '', this); @@ -1709,24 +1734,6 @@ export class ChatWidget extends Disposable implements IChatWidget { }, }); })); - this._register(autorun(reader => { - this.input.inputPartHeight.read(reader); - if (!this.listWidget) { - // This is set up before the list/renderer are created - return; - } - - const editedRequest = this.listWidget.getTemplateDataForRequestId(this.viewModel?.editing?.id); - if (isRequestVM(editedRequest?.currentElement) && this.viewModel?.editing) { - this.listWidget.updateItemHeightOnRender(editedRequest?.currentElement, editedRequest); - } - - if (this.bodyDimension) { - this.layout(this.bodyDimension.height, this.bodyDimension.width); - } - - this._onDidChangeContentHeight.fire(); - })); this._register(this.inputEditor.onDidChangeModelContent(() => { this.parsedChatRequest = undefined; this.updateChatInputContext(); @@ -2207,7 +2214,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.inputPart.layout(width); - const inputHeight = this.inputPart.inputPartHeight.get(); + const inputHeight = this.inputPart.height.get(); const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height; const lastElementVisible = this.listWidget.isScrolledToBottom; const lastItem = this.listWidget.lastItem; @@ -2256,7 +2263,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const possibleMaxHeight = (this._dynamicMessageLayoutData?.maxHeight ?? maxHeight); const width = this.bodyDimension?.width ?? this.container.offsetWidth; this.input.layout(width); - const inputPartHeight = this.input.inputPartHeight.get(); + const inputPartHeight = this.input.height.get(); const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height; const newHeight = Math.min(renderHeight + diff, possibleMaxHeight - inputPartHeight - chatSuggestNextWidgetHeight); this.layout(newHeight + inputPartHeight + chatSuggestNextWidgetHeight, width); @@ -2301,7 +2308,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const width = this.bodyDimension?.width ?? this.container.offsetWidth; this.input.layout(width); - const inputHeight = this.input.inputPartHeight.get(); + const inputHeight = this.input.height.get(); const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height; const totalMessages = this.viewModel.getItems(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts index 15fe5e4c7a681..6af972fcf3ac3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatContextUsageWidget.ts @@ -123,7 +123,7 @@ export class ChatContextUsageWidget extends Disposable { this._currentModel = model; this._modelListener.clear(); - if (model) { + if (model && !model.contributedChatSession) { this._modelListener.value = model.onDidChange(() => { this._updateScheduler.schedule(); }); 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 7ba16c7bd47a8..c9176cd841623 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -271,7 +271,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private readonly _widgetController = this._register(new MutableDisposable()); private readonly _contextUsageWidget = this._register(new MutableDisposable()); - readonly inputPartHeight = observableValue(this, 0); + readonly height = observableValue(this, 0); private _inputEditor!: CodeEditorWidget; private _inputEditorElement!: HTMLElement; @@ -2026,7 +2026,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const inputResizeObserver = this._register(new dom.DisposableResizeObserver(() => { const newHeight = this.container.offsetHeight; - this.inputPartHeight.set(newHeight, undefined); + this.height.set(newHeight, undefined); })); inputResizeObserver.observe(this.container); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index 4faa99244a995..d31643faecdb1 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -108,7 +108,10 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem { const modes = chatModeService.getModes(); const currentMode = delegate.currentMode.get(); const agentMode = modes.builtin.find(mode => mode.id === ChatMode.Agent.id); - const otherBuiltinModes = modes.builtin.filter(mode => mode.id !== ChatMode.Agent.id); + + const shouldHideEditMode = configurationService.getValue(ChatConfiguration.EditModeHidden) && chatAgentService.hasToolsAgent && currentMode.id !== ChatMode.Edit.id; + + const otherBuiltinModes = modes.builtin.filter(mode => mode.id !== ChatMode.Agent.id && !(shouldHideEditMode && mode.id === ChatMode.Edit.id)); const customModes = groupBy( modes.custom, mode => isModeConsideredBuiltIn(mode, this._productService) ? 'builtin' : 'custom'); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts index 8e5feb2fd94b1..3d5234a740b81 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts @@ -57,12 +57,15 @@ function modelDelegateToWidgetActionsProvider(delegate: IModelPickerDelegate, te checked: true, category: DEFAULT_MODEL_PICKER_CATEGORY, class: undefined, + description: localize('chat.modelPicker.auto.detail', "Best for your request based on capacity and performance."), tooltip: localize('chat.modelPicker.auto', "Auto"), label: localize('chat.modelPicker.auto', "Auto"), + hover: { content: localize('chat.modelPicker.auto.description', "Automatically selects the best model for your task based on context and complexity.") }, run: () => { } } satisfies IActionWidgetDropdownAction]; } return models.map(model => { + const hoverContent = model.metadata.tooltip; return { id: model.metadata.id, enabled: true, @@ -71,7 +74,8 @@ function modelDelegateToWidgetActionsProvider(delegate: IModelPickerDelegate, te category: model.metadata.modelPickerCategory || DEFAULT_MODEL_PICKER_CATEGORY, class: undefined, description: model.metadata.detail, - tooltip: model.metadata.tooltip ?? model.metadata.name, + tooltip: hoverContent ? '' : model.metadata.name, + hover: hoverContent ? { content: hoverContent } : undefined, label: model.metadata.name, run: () => { const previousModel = delegate.getCurrentModel(); 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 2036f2ffddc3b..0e334221dc7bb 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -661,7 +661,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { // When showing sessions stacked, adjust the height of the sessions list to make room for chat input this._register(autorun(reader => { - chatWidget.input.inputPartHeight.read(reader); + chatWidget.input.height.read(reader); if (this.sessionsViewerVisible && this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { this.relayout(); } @@ -966,7 +966,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { let availableSessionsHeight = height - this.sessionsTitleContainer.offsetHeight; if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { - availableSessionsHeight -= Math.max(ChatViewPane.MIN_CHAT_WIDGET_HEIGHT, this._widget?.input?.inputPartHeight.get() ?? 0); + availableSessionsHeight -= Math.max(ChatViewPane.MIN_CHAT_WIDGET_HEIGHT, this._widget?.input?.height.get() ?? 0); } else { availableSessionsHeight -= this.sessionsNewButtonContainer?.offsetHeight ?? 0; } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index 92041da758644..eb181252df953 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -773,7 +773,6 @@ export interface IChatSubagentToolInvocationData { export interface IChatTodoListContent { kind: 'todoList'; - sessionId: string; todoList: Array<{ id: string; title: string; diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 075e980921f11..dce1f75f7a0e3 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -12,6 +12,7 @@ export enum ChatConfiguration { AgentEnabled = 'chat.agent.enabled', AgentStatusEnabled = 'chat.agentsControl.enabled', AgentSessionProjectionEnabled = 'chat.agentSessionProjection.enabled', + EditModeHidden = 'chat.editMode.hidden', Edits2Enabled = 'chat.edits2.enabled', ExtensionToolsEnabled = 'chat.extensionTools.enabled', RepoInfoEnabled = 'chat.repoInfo.enabled', diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 66a79b87c222f..d5abc6281efc2 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -285,7 +285,7 @@ export interface ILanguageModelsService { readonly _serviceBrand: undefined; - // TODO @lramos15 - Make this a richer event in the future. Right now it just indicates some change happened, but not what + readonly onDidChangeLanguageModelVendors: Event; readonly onDidChangeLanguageModels: Event; updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void; @@ -306,6 +306,8 @@ export interface ILanguageModelsService { registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable; + deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any sendChatRequest(modelId: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; @@ -415,6 +417,9 @@ export class LanguageModelsService implements ILanguageModelsService { private readonly _providers = new Map(); private readonly _vendors = new Map(); + private readonly _onDidChangeLanguageModelVendors = this._store.add(new Emitter()); + readonly onDidChangeLanguageModelVendors = this._onDidChangeLanguageModelVendors.event; + private readonly _modelsGroups = new Map(); private readonly _modelCache = new Map(); private readonly _resolveLMSequencer = new SequencerByKey(); @@ -440,11 +445,11 @@ export class LanguageModelsService implements ILanguageModelsService { this._store.add(this.onDidChangeLanguageModels(() => this._hasUserSelectableModels.set(this._modelCache.size > 0 && Array.from(this._modelCache.values()).some(model => model.isUserSelectable)))); this._store.add(this._languageModelsConfigurationService.onDidChangeLanguageModelGroups(changedGroups => this._onDidChangeLanguageModelGroups(changedGroups))); - this._store.add(languageModelChatProviderExtensionPoint.setHandler((extensions) => { + this._store.add(languageModelChatProviderExtensionPoint.setHandler((extensions, { added, removed }) => { + const addedVendors: IUserFriendlyLanguageModel[] = []; + const removedVendors: IUserFriendlyLanguageModel[] = []; - this._vendors.clear(); - - for (const extension of extensions) { + for (const extension of added) { for (const item of Iterable.wrap(extension.value)) { if (this._vendors.has(item.vendor)) { extension.collector.error(localize('vscode.extension.contributes.languageModels.vendorAlreadyRegistered', "The vendor '{0}' is already registered and cannot be registered twice", item.vendor)); @@ -458,21 +463,76 @@ export class LanguageModelsService implements ILanguageModelsService { extension.collector.error(localize('vscode.extension.contributes.languageModels.whitespaceVendor', "The vendor field cannot start or end with whitespace.")); continue; } - this._vendors.set(item.vendor, item); - // Have some models we want from this vendor, so activate the extension - if (this._hasStoredModelForVendor(item.vendor)) { - this._extensionService.activateByEvent(`onLanguageModelChatProvider:${item.vendor}`); - } + addedVendors.push(item); } } - for (const [vendor, _] of this._providers) { - if (!this._vendors.has(vendor)) { - this._providers.delete(vendor); + + for (const extension of removed) { + for (const item of Iterable.wrap(extension.value)) { + removedVendors.push(item); } } + + this.deltaLanguageModelChatProviderDescriptors(addedVendors, removedVendors); })); } + deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void { + const addedVendorIds: string[] = []; + const removedVendorIds: string[] = []; + + for (const item of added) { + if (this._vendors.has(item.vendor)) { + this._logService.error(`The vendor '${item.vendor}' is already registered and cannot be registered twice`); + continue; + } + if (isFalsyOrWhitespace(item.vendor)) { + this._logService.error('The vendor field cannot be empty.'); + continue; + } + if (item.vendor.trim() !== item.vendor) { + this._logService.error('The vendor field cannot start or end with whitespace.'); + continue; + } + // Cast to IUserFriendlyLanguageModel - fill in optional properties with undefined + const vendor: IUserFriendlyLanguageModel = { + vendor: item.vendor, + displayName: item.displayName, + configuration: item.configuration, + managementCommand: item.managementCommand, + when: item.when + }; + this._vendors.set(item.vendor, vendor); + addedVendorIds.push(item.vendor); + // Have some models we want from this vendor, so activate the extension + if (this._hasStoredModelForVendor(item.vendor)) { + this._extensionService.activateByEvent(`onLanguageModelChatProvider:${item.vendor}`); + } + } + + for (const item of removed) { + this._vendors.delete(item.vendor); + this._providers.delete(item.vendor); + this._clearModelCache(item.vendor); + removedVendorIds.push(item.vendor); + } + + for (const [vendor, _] of this._providers) { + if (!this._vendors.has(vendor)) { + this._providers.delete(vendor); + } + } + + if (addedVendorIds.length > 0 || removedVendorIds.length > 0) { + this._onDidChangeLanguageModelVendors.fire([...addedVendorIds, ...removedVendorIds]); + if (removedVendorIds.length > 0) { + for (const vendor of removedVendorIds) { + this._onLanguageModelChange.fire(vendor); + } + } + } + } + private async _onDidChangeLanguageModelGroups(changedGroups: readonly ILanguageModelsProviderGroup[]): Promise { const changedVendors = new Set(changedGroups.map(g => g.vendor)); await Promise.all(Array.from(changedVendors).map(vendor => this._resolveAllLanguageModels(vendor, true))); diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index bae8c889b1d26..875f3abf2839e 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -864,6 +864,10 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel } public set shouldBeRemovedOnSend(disablement: IChatRequestDisablement | undefined) { + if (this._shouldBeRemovedOnSend === disablement) { + return; + } + this._shouldBeRemovedOnSend = disablement; this._onDidChange.fire(defaultChatResponseModelChangeReason); } diff --git a/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts index d434ea202e8cb..d36593cb3bf53 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts @@ -75,8 +75,6 @@ export interface IChatViewModel { export interface IChatRequestViewModel { readonly id: string; - /** @deprecated */ - readonly sessionId: string; readonly sessionResource: URI; /** This ID updates every time the underlying data changes */ readonly dataId: string; @@ -187,8 +185,6 @@ export interface IChatResponseViewModel { readonly model: IChatResponseModel; readonly id: string; readonly session: IChatViewModel; - /** @deprecated */ - readonly sessionId: string; readonly sessionResource: URI; /** This ID updates every time the underlying data changes */ readonly dataId: string; @@ -366,11 +362,6 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return `${this.id}_${this._model.version + (this._model.response?.isComplete ? 1 : 0)}`; } - /** @deprecated */ - get sessionId() { - return this._model.session.sessionId; - } - get sessionResource() { return this._model.session.sessionResource; } @@ -462,11 +453,6 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi (this.isLast ? '_last' : ''); } - /** @deprecated */ - get sessionId() { - return this._model.session.sessionId; - } - get sessionResource(): URI { return this._model.session.sessionResource; } diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts index 88638500cd305..895a6c537f560 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts @@ -23,7 +23,6 @@ import { IChatTodo, IChatTodoListService } from '../chatTodoListService.js'; import { localize } from '../../../../../../nls.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { URI } from '../../../../../../base/common/uri.js'; -import { chatSessionResourceToId, LocalChatSessionUri } from '../../model/chatUri.js'; export const ManageTodoListToolToolId = 'manage_todo_list'; @@ -81,7 +80,6 @@ interface IManageTodoListToolInputParams { title: string; status: 'not-started' | 'in-progress' | 'completed'; }>; - chatSessionId?: string; } export class ManageTodoListTool extends Disposable implements IToolImpl { @@ -97,17 +95,23 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { // eslint-disable-next-line @typescript-eslint/no-explicit-any async invoke(invocation: IToolInvocation, _countTokens: any, _progress: any, _token: CancellationToken): Promise { const args = invocation.parameters as IManageTodoListToolInputParams; - // For: #263001 Use default sessionId - const DEFAULT_TODO_SESSION_ID = 'default'; - const chatSessionId = invocation.context?.sessionId ?? args.chatSessionId ?? DEFAULT_TODO_SESSION_ID; + const chatSessionResource = invocation.context?.sessionResource; + if (!chatSessionResource) { + return { + content: [{ + kind: 'text', + value: 'Error: No session resource available' + }] + }; + } this.logService.debug(`ManageTodoListTool: Invoking with options ${JSON.stringify(args)}`); try { if (args.operation === 'read') { - return this.handleReadOperation(LocalChatSessionUri.forSession(chatSessionId)); + return this.handleReadOperation(chatSessionResource); } else { - return this.handleWriteOperation(args, LocalChatSessionUri.forSession(chatSessionId)); + return this.handleWriteOperation(args, chatSessionResource); } } catch (error) { @@ -123,11 +127,12 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { const args = context.parameters as IManageTodoListToolInputParams; - // For: #263001 Use default sessionId - const DEFAULT_TODO_SESSION_ID = 'default'; - const chatSessionId = context.chatSessionId ?? args.chatSessionId ?? DEFAULT_TODO_SESSION_ID; + const chatSessionResource = context.chatSessionResource; + if (!chatSessionResource) { + return undefined; + } - const currentTodoItems = this.chatTodoListService.getTodos(LocalChatSessionUri.forSession(chatSessionId)); + const currentTodoItems = this.chatTodoListService.getTodos(chatSessionResource); let message: string | undefined; if (args.operation === 'read') { @@ -147,7 +152,6 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { pastTenseMessage: new MarkdownString(message ?? localize('todo.updatedList', "Updated todo list")), toolSpecificData: { kind: 'todoList', - sessionId: chatSessionId, todoList: todoList } }; @@ -222,8 +226,7 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { operation: 'read', notStartedCount: statusCounts.notStartedCount, inProgressCount: statusCounts.inProgressCount, - completedCount: statusCounts.completedCount, - chatSessionId: chatSessionResourceToId(chatSessionResource) + completedCount: statusCounts.completedCount } ); @@ -276,8 +279,7 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { operation: 'write', notStartedCount: statusCounts.notStartedCount, inProgressCount: statusCounts.inProgressCount, - completedCount: statusCounts.completedCount, - chatSessionId: chatSessionResourceToId(chatSessionResource) + completedCount: statusCounts.completedCount } ); @@ -349,7 +351,6 @@ type TodoListToolInvokedEvent = { notStartedCount: number; inProgressCount: number; completedCount: number; - chatSessionId: string | undefined; }; type TodoListToolInvokedClassification = { @@ -357,7 +358,6 @@ type TodoListToolInvokedClassification = { notStartedCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tasks with not-started status.' }; inProgressCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tasks with in-progress status.' }; completedCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tasks with completed status.' }; - chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat session that the tool was used within, if applicable.' }; owner: 'bhavyaus'; comment: 'Provides insight into the usage of the todo list tool including detailed task status distribution.'; }; diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts index 7d7e0c86fd9a9..a153f3b842525 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -162,7 +162,7 @@ export interface IToolInvocationPreparationContext { chatRequestId?: string; /** @deprecated Use {@link chatSessionResource} instead */ chatSessionId?: string; - chatSessionResource?: URI; + chatSessionResource: URI | undefined; chatInteractionId?: string; } @@ -390,7 +390,7 @@ export interface ILanguageModelToolsService { readonly readToolSet: ToolSet; readonly agentToolSet: ToolSet; readonly onDidChangeTools: Event; - readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ readonly sessionId: string; readonly toolData: IToolData }>; + readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ readonly sessionResource: URI; readonly toolData: IToolData }>; registerToolData(toolData: IToolData): IDisposable; registerToolImplementation(id: string, tool: IToolImpl): IDisposable; registerTool(toolData: IToolData, tool: IToolImpl): IDisposable; diff --git a/src/vs/workbench/contrib/chat/electron-browser/builtInTools/fetchPageTool.ts b/src/vs/workbench/contrib/chat/electron-browser/builtInTools/fetchPageTool.ts index 43406a4e9293c..d59f63292d7e9 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/builtInTools/fetchPageTool.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/builtInTools/fetchPageTool.ts @@ -16,7 +16,6 @@ import { IWebContentExtractorService, WebContentExtractResult } from '../../../. import { detectEncodingFromBuffer } from '../../../../services/textfile/common/encoding.js'; import { ITrustedDomainService } from '../../../url/browser/trustedDomainService.js'; import { IChatService } from '../../common/chatService/chatService.js'; -import { LocalChatSessionUri } from '../../common/model/chatUri.js'; import { ChatImageMimeType } from '../../common/languageModels.js'; import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, IToolResultDataPart, IToolResultTextPart, ToolDataSource, ToolProgress } from '../../common/tools/languageModelToolsService.js'; import { InternalFetchWebPageToolId } from '../../common/tools/builtinTools/tools.js'; @@ -219,8 +218,8 @@ export class FetchWebPageTool implements IToolImpl { } let confirmationNotNeededReason: string | undefined; - if (context.chatSessionId) { - const model = this._chatService.getSession(LocalChatSessionUri.forSession(context.chatSessionId)); + if (context.chatSessionResource) { + const model = this._chatService.getSession(context.chatSessionResource); const userMessages = model?.getRequests().map(r => r.message.text.toLowerCase()); let urlsMentionedInPrompt = false; for (const uri of urlsNeedingConfirmation) { diff --git a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts index dcb88c2a6ec00..728883ddfe144 100644 --- a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts @@ -100,7 +100,6 @@ suite('ChatResponseAccessibleView', () => { test('returns todo list description for todoList data', () => { const todoData: IChatTodoListContent = { kind: 'todoList', - sessionId: 'session-1', todoList: [ { id: '1', title: 'Task 1', status: 'in-progress' }, { id: '2', title: 'Task 2', status: 'completed' } @@ -117,7 +116,6 @@ suite('ChatResponseAccessibleView', () => { test('returns empty for empty todo list', () => { const todoData: IChatTodoListContent = { kind: 'todoList', - sessionId: 'session-1', todoList: [] }; assert.strictEqual(getToolSpecificDataDescription(todoData), ''); 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 0e3ebc1a642dc..f30faad4f266c 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 @@ -25,6 +25,9 @@ class MockLanguageModelsService implements ILanguageModelsService { private readonly _onDidChangeLanguageModels = new Emitter(); readonly onDidChangeLanguageModels = this._onDidChangeLanguageModels.event; + private readonly _onDidChangeLanguageModelVendors = new Emitter(); + readonly onDidChangeLanguageModelVendors = this._onDidChangeLanguageModelVendors.event; + addVendor(vendor: IUserFriendlyLanguageModel): void { this.vendors.push(vendor); this.modelsByVendor.set(vendor.vendor, []); @@ -56,6 +59,10 @@ class MockLanguageModelsService implements ILanguageModelsService { throw new Error('Method not implemented.'); } + deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void { + throw new Error('Method not implemented.'); + } + updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void { const metadata = this.models.get(modelIdentifier); if (metadata) { diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index a18dc6c871980..48e2b40d1f17f 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -10,9 +10,8 @@ import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; -import { ChatMessageRole, languageModelChatProviderExtensionPoint, LanguageModelsService, IChatMessage, IChatResponsePart, ILanguageModelChatMetadata } from '../../common/languageModels.js'; +import { ChatMessageRole, LanguageModelsService, IChatMessage, IChatResponsePart, ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; -import { ExtensionsRegistry } from '../../../../services/extensions/common/extensionsRegistry.js'; import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../common/widget/input/modelPickerWidget.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; import { TestStorageService } from '../../../../test/common/workbenchTestServices.js'; @@ -53,17 +52,10 @@ suite('LanguageModels', function () { new TestSecretStorageService(), ); - const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; - - ext.acceptUsers([{ - description: { ...nullExtensionDescription }, - value: { vendor: 'test-vendor' }, - collector: null! - }, { - description: { ...nullExtensionDescription }, - value: { vendor: 'actual-vendor' }, - collector: null! - }]); + languageModels.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'test-vendor', displayName: 'Test Vendor', configuration: undefined, managementCommand: undefined, when: undefined }, + { vendor: 'actual-vendor', displayName: 'Actual Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); store.add(languageModels.registerLanguageModelProvider('test-vendor', { onDidChange: Event.None, @@ -189,12 +181,9 @@ suite('LanguageModels', function () { })); // Register the extension point for the actual vendor - const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; - ext.acceptUsers([{ - description: { ...nullExtensionDescription }, - value: { vendor: 'actual-vendor' }, - collector: null! - }]); + languageModels.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'actual-vendor', displayName: 'Actual Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); const models = await languageModels.selectLanguageModels({ id: 'actual-lm' }); assert.ok(models.length === 1); @@ -264,21 +253,11 @@ suite('LanguageModels - When Clause', function () { new TestSecretStorageService(), ); - const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; - - ext.acceptUsers([{ - description: { ...nullExtensionDescription }, - value: { vendor: 'visible-vendor', displayName: 'Visible Vendor' }, - collector: null! - }, { - description: { ...nullExtensionDescription }, - value: { vendor: 'conditional-vendor', displayName: 'Conditional Vendor', when: 'testKey' }, - collector: null! - }, { - description: { ...nullExtensionDescription }, - value: { vendor: 'hidden-vendor', displayName: 'Hidden Vendor', when: 'falseKey' }, - collector: null! - }]); + languageModelsWithWhen.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'visible-vendor', displayName: 'Visible Vendor', configuration: undefined, managementCommand: undefined, when: undefined }, + { vendor: 'conditional-vendor', displayName: 'Conditional Vendor', configuration: undefined, managementCommand: undefined, when: 'testKey' }, + { vendor: 'hidden-vendor', displayName: 'Hidden Vendor', configuration: undefined, managementCommand: undefined, when: 'falseKey' } + ], []); }); teardown(function () { @@ -304,6 +283,7 @@ suite('LanguageModels - When Clause', function () { const vendors = languageModelsWithWhen.getVendors(); assert.ok(!vendors.some(v => v.vendor === 'hidden-vendor'), 'hidden-vendor should be hidden when falseKey is false'); }); + }); suite('LanguageModels - Model Picker Preferences Storage', function () { @@ -335,12 +315,9 @@ suite('LanguageModels - Model Picker Preferences Storage', function () { ); // Register vendor1 used in most tests - const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; - ext.acceptUsers([{ - description: { ...nullExtensionDescription }, - value: { vendor: 'vendor1' }, - collector: null! - }]); + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'vendor1', displayName: 'Vendor 1', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); disposables.add(languageModelsService.registerLanguageModelProvider('vendor1', { onDidChange: Event.None, @@ -446,12 +423,9 @@ suite('LanguageModels - Model Picker Preferences Storage', function () { test('only fires onChange event for affected vendors', async function () { // Register vendor2 - const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; - ext.acceptUsers([{ - description: { ...nullExtensionDescription }, - value: { vendor: 'vendor2' }, - collector: null! - }]); + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'vendor2', displayName: 'Vendor 2', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); disposables.add(languageModelsService.registerLanguageModelProvider('vendor2', { onDidChange: Event.None, @@ -556,12 +530,6 @@ suite('LanguageModels - Model Change Events', function () { setup(async function () { storageService = new TestStorageService(); - const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; - ext.acceptUsers([{ - description: { ...nullExtensionDescription }, - value: { vendor: 'test-vendor' }, - collector: null! - }]); languageModelsService = new LanguageModelsService( new class extends mock() { @@ -581,6 +549,11 @@ suite('LanguageModels - Model Change Events', function () { new class extends mock() { }, new TestSecretStorageService(), ); + + // Register the vendor first + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'test-vendor', displayName: 'Test Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); }); teardown(function () { @@ -898,3 +871,113 @@ suite('LanguageModels - Model Change Events', function () { assert.strictEqual(eventFired, true, 'Should fire event when models change even without provider change event'); }); }); + +suite('LanguageModels - Vendor Change Events', function () { + + let languageModelsService: LanguageModelsService; + const disposables = new DisposableStore(); + + setup(function () { + languageModelsService = new LanguageModelsService( + new class extends mock() { + override activateByEvent(name: string) { + return Promise.resolve(); + } + }, + new NullLogService(), + new TestStorageService(), + new MockContextKeyService(), + new class extends mock() { + override onDidChangeLanguageModelGroups = Event.None; + override getLanguageModelsProviderGroups() { + return []; + } + }, + new class extends mock() { }, + new TestSecretStorageService(), + ); + }); + + teardown(function () { + languageModelsService.dispose(); + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('fires onDidChangeLanguageModelVendors when a vendor is added', async function () { + const eventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors))); + }); + + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'added-vendor', displayName: 'Added Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); + + const vendors = await eventPromise; + assert.ok(vendors.includes('added-vendor')); + }); + + test('fires onDidChangeLanguageModelVendors when a vendor is removed', async function () { + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'removed-vendor', displayName: 'Removed Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); + + const eventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors))); + }); + + languageModelsService.deltaLanguageModelChatProviderDescriptors([], [ + { vendor: 'removed-vendor', displayName: 'Removed Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ]); + + const vendors = await eventPromise; + assert.ok(vendors.includes('removed-vendor')); + }); + + test('fires onDidChangeLanguageModelVendors when multiple vendors are added and removed', async function () { + // Add multiple vendors + const addEventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors))); + }); + + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'vendor-a', displayName: 'Vendor A', configuration: undefined, managementCommand: undefined, when: undefined }, + { vendor: 'vendor-b', displayName: 'Vendor B', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); + + const addedVendors = await addEventPromise; + assert.ok(addedVendors.includes('vendor-a')); + assert.ok(addedVendors.includes('vendor-b')); + + // Remove one vendor + const removeEventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors))); + }); + + languageModelsService.deltaLanguageModelChatProviderDescriptors([], [ + { vendor: 'vendor-a', displayName: 'Vendor A', configuration: undefined, managementCommand: undefined, when: undefined } + ]); + + const removedVendors = await removeEventPromise; + assert.ok(removedVendors.includes('vendor-a')); + }); + + test('does not fire onDidChangeLanguageModelVendors when no vendors are added or removed', async function () { + // Add initial vendor + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'stable-vendor', displayName: 'Stable Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); + + // Listen for change event + let eventFired = false; + disposables.add(languageModelsService.onDidChangeLanguageModelVendors(() => { + eventFired = true; + })); + + // Call with empty arrays - should not fire event + languageModelsService.deltaLanguageModelChatProviderDescriptors([], []); + + assert.strictEqual(eventFired, false, 'Should not fire event when vendor list is unchanged'); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.ts index fd336e7fb3752..57c83ec713113 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.ts @@ -18,7 +18,11 @@ export class NullLanguageModelsService implements ILanguageModelsService { return Disposable.None; } + deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void { + } + onDidChangeLanguageModels = Event.None; + onDidChangeLanguageModelVendors = Event.None; updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void { return; 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 7ee177ea7313c..73df30cb679d9 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts @@ -14,6 +14,7 @@ import { IVariableReference } from '../../../common/chatModes.js'; import { IChatToolInvocation } from '../../../common/chatService/chatService.js'; import { ChatRequestToolReferenceEntry } from '../../../common/attachments/chatVariableEntries.js'; import { CountTokensCallback, IBeginToolCallOptions, ILanguageModelToolsService, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolSet } from '../../../common/tools/languageModelToolsService.js'; +import { URI } from '../../../../../../base/common/uri.js'; export class MockLanguageModelToolsService implements ILanguageModelToolsService { _serviceBrand: undefined; @@ -25,7 +26,7 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService constructor() { } readonly onDidChangeTools: Event = Event.None; - readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ sessionId: string; toolData: IToolData }> = Event.None; + readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ sessionResource: URI; toolData: IToolData }> = Event.None; registerToolData(toolData: IToolData): IDisposable { return Disposable.None; diff --git a/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts b/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts index efcb9f37fe17a..acee94cc06c5a 100644 --- a/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts @@ -18,6 +18,7 @@ import { InternalFetchWebPageToolId } from '../../../../common/tools/builtinTool import { MockChatService } from '../../../common/chatService/mockChatService.js'; import { upcastDeepPartial } from '../../../../../../../base/test/common/mock.js'; import { IChatService } from '../../../../common/chatService/chatService.js'; +import { LocalChatSessionUri } from '../../../../common/model/chatUri.js'; class TestWebContentExtractorService implements IWebContentExtractorService { _serviceBrand: undefined; @@ -191,7 +192,7 @@ suite('FetchWebPageTool', () => { ); const preparation = await tool.prepareToolInvocation( - { parameters: { urls: ['https://valid.com', 'test://valid/resource', 'invalid://invalid'] } }, + { parameters: { urls: ['https://valid.com', 'test://valid/resource', 'invalid://invalid'] }, chatSessionResource: undefined }, CancellationToken.None ); @@ -229,7 +230,7 @@ suite('FetchWebPageTool', () => { ); const preparation1 = await tool.prepareToolInvocation( - { parameters: { urls: ['https://example.com'] }, chatSessionId: 'a' }, + { parameters: { urls: ['https://example.com'] }, chatSessionResource: LocalChatSessionUri.forSession('a') }, CancellationToken.None ); @@ -237,7 +238,7 @@ suite('FetchWebPageTool', () => { assert.strictEqual(preparation1.confirmationMessages?.title, undefined); const preparation2 = await tool.prepareToolInvocation( - { parameters: { urls: ['https://other.com'] }, chatSessionId: 'a' }, + { parameters: { urls: ['https://other.com'] }, chatSessionResource: LocalChatSessionUri.forSession('a') }, CancellationToken.None ); diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetryReporter.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetryReporter.ts index 63bd405804233..387563fbd4af2 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetryReporter.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetryReporter.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { TimeoutTimer } from '../../../../../base/common/async.js'; -import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; import { IObservableWithChange, IObservable, runOnChange } from '../../../../../base/common/observable.js'; import { BaseStringEdit } from '../../../../../editor/common/core/edits/stringEdit.js'; import { StringText } from '../../../../../editor/common/core/text/abstractText.js'; @@ -25,17 +25,13 @@ export class ArcTelemetryReporter extends Disposable { private readonly _gitRepo: IObservable, private readonly _trackedEdit: BaseStringEdit, private readonly _sendTelemetryEvent: (res: ArcTelemetryReporterData) => void, - private readonly _onBeforeDispose: () => void, + private readonly _dispose: () => void, @ITelemetryService private readonly _telemetryService: ITelemetryService ) { super(); this._arcTracker = new ArcTracker(this._documentValueBeforeTrackedEdit, this._trackedEdit); - this._store.add(toDisposable(() => { - this._onBeforeDispose(); - })); - this._store.add(runOnChange(this._document.value, (_val, _prevVal, changes) => { const edit = BaseStringEdit.composeOrUndefined(changes.map(c => c.edit)); if (edit) { @@ -54,7 +50,7 @@ export class ArcTelemetryReporter extends Disposable { this._report(timeMs); } else { this._reportAfter(timeMs, i === this._timesMs.length - 1 ? () => { - this.dispose(); + this._dispose(); } : undefined); } } diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts index d369742b365b0..faa9c30d06c23 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts @@ -95,7 +95,7 @@ export class EditTelemetryReportInlineEditArcSender extends Disposable { ...forwardToChannelIf(isCopilotLikeExtension(data.$extensionId)), }); }, () => { - this._store.deleteAndLeak(reporter); + this._store.delete(reporter); })); })); } @@ -255,7 +255,7 @@ export class EditTelemetryReportEditArcForChatOrInlineChatSender extends Disposa ...forwardToChannelIf(isCopilotLikeExtension(data.props.$extensionId)), }); }, () => { - this._store.deleteAndLeak(reporter); + this._store.delete(reporter); })); })); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index d88b121fadd16..fcf646a510990 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -393,7 +393,7 @@ export class InlineChatWidget { let value = this.contentHeight; value -= this._chatWidget.contentHeight; - value += Math.min(this._chatWidget.input.inputPartHeight.get() + maxWidgetOutputHeight, this._chatWidget.contentHeight); + value += Math.min(this._chatWidget.input.height.get() + maxWidgetOutputHeight, this._chatWidget.contentHeight); return value; }