diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts index d7f0000b99b..48c6be29680 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorOverlay.ts @@ -394,7 +394,7 @@ class ChatEditingOverlayController { const { session, entry } = data; - if (!session.isGlobalEditingSession && !configurationService.getValue(InlineChatConfigKeys.ShowGutterMenu)) { + if (!session.isGlobalEditingSession && configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'zone') { // inline chat with zone UI - no need for chat overlay hide(); return; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts new file mode 100644 index 00000000000..5b8a4bba6f7 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun, debouncedObservable, derived, observableValue } from '../../../../base/common/observable.js'; +import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { InlineChatConfigKeys } from '../common/inlineChat.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; +import { InlineChatEditorAffordance } from './inlineChatEditorAffordance.js'; +import { InlineChatOverlayWidget } from './inlineChatOverlayWidget.js'; +import { InlineChatGutterAffordance } from './inlineChatGutterAffordance.js'; +import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { assertType } from '../../../../base/common/types.js'; +import { Event } from '../../../../base/common/event.js'; + +export class InlineChatAffordance extends Disposable { + + private readonly _overlayWidget: InlineChatOverlayWidget; + + private _menuData = observableValue<{ rect: DOMRect; above: boolean } | undefined>(this, undefined); + + + constructor( + private readonly _editor: ICodeEditor, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService, + @IChatEntitlementService chatEntiteldService: IChatEntitlementService, + ) { + super(); + + // Create the overlay widget once, owned by this class + this._overlayWidget = this._store.add(this._instantiationService.createInstance(InlineChatOverlayWidget, this._editor)); + + const affordance = observableConfigValue<'off' | 'gutter' | 'editor'>(InlineChatConfigKeys.Affordance, 'off', configurationService); + + const editorObs = observableCodeEditor(this._editor); + + const suppressGutter = observableValue(this, false); + + const debouncedSelection = debouncedObservable(editorObs.cursorSelection, 500); + + const selection = observableValue(this, undefined); + + + this._store.add(autorun(r => { + const value = editorObs.cursorSelection.read(r); + if (!value || value.isEmpty()) { + selection.set(undefined, undefined); + } + })); + + + this._store.add(autorun(r => { + if (chatEntiteldService.sentimentObs.read(r).hidden) { + selection.set(undefined, undefined); + return; + } + if (suppressGutter.read(r)) { + selection.set(undefined, undefined); + return; + } + const value = debouncedSelection.read(r); + if (!value || value.isEmpty()) { + selection.set(undefined, undefined); + return; + } + selection.set(value, undefined); + })); + + + + // Instantiate the gutter indicator + this._store.add(this._instantiationService.createInstance( + InlineChatGutterAffordance, + editorObs, + derived(r => affordance.read(r) === 'gutter' ? selection.read(r) : undefined), + suppressGutter, + this._menuData + )); + + // Create content widget (alternative to gutter indicator) + this._store.add(this._instantiationService.createInstance( + InlineChatEditorAffordance, + this._editor, + derived(r => affordance.read(r) === 'editor' ? selection.read(r) : undefined), + suppressGutter, + this._menuData + )); + + // Reset suppressGutter when the selection changes + this._store.add(autorun(reader => { + editorObs.cursorSelection.read(reader); + suppressGutter.set(false, undefined); + })); + + this._store.add(autorun(r => { + const data = this._menuData.read(r); + if (!data) { + return; + } + + const editorDomNode = this._editor.getDomNode()!; + const editorRect = editorDomNode.getBoundingClientRect(); + const padding = 1; + + let top: number; + if (data.above) { + // Pass the top of the gutter indicator minus padding + top = data.rect.top - editorRect.top - padding; + } else { + // Menu appears below - position at bottom of gutter indicator + top = data.rect.bottom - editorRect.top + padding; + } + const left = data.rect.left - editorRect.left; + + // Show the overlay widget + this._overlayWidget.show(top, left, data.above); + })); + + this._store.add(this._overlayWidget.onDidHide(() => { + suppressGutter.set(true, undefined); + this._menuData.set(undefined, undefined); + this._editor.focus(); + })); + } + + async showMenuAtSelection() { + assertType(this._editor.hasModel()); + + const direction = this._editor.getSelection().getDirection(); + const position = this._editor.getPosition(); + const editorDomNode = this._editor.getDomNode(); + const scrolledPosition = this._editor.getScrolledVisiblePosition(position); + const editorRect = editorDomNode.getBoundingClientRect(); + const x = editorRect.left + scrolledPosition.left; + const y = editorRect.top + scrolledPosition.top; + + this._menuData.set({ + rect: new DOMRect(x, y, 0, scrolledPosition.height), + above: direction === SelectionDirection.RTL + }, undefined); + + await Event.toPromise(this._overlayWidget.onDidHide); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 5b164f22167..09df9d10aa2 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -51,7 +51,7 @@ import { INotebookEditorService } from '../../notebook/browser/services/notebook import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../notebook/common/notebookService.js'; import { CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; -import { InlineChatSelectionIndicator } from './inlineChatSelectionGutterIndicator.js'; +import { InlineChatAffordance } from './inlineChatAffordance.js'; import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; @@ -109,9 +109,9 @@ export class InlineChatController implements IEditorContribution { private readonly _store = new DisposableStore(); private readonly _isActiveController = observableValue(this, false); - private readonly _showGutterMenu: IObservable; + private readonly _renderMode: IObservable<'zone' | 'hover'>; private readonly _zone: Lazy; - private readonly _gutterIndicator: InlineChatSelectionIndicator; + private readonly _gutterIndicator: InlineChatAffordance; private readonly _currentSession: IObservable; @@ -141,9 +141,9 @@ export class InlineChatController implements IEditorContribution { const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService); - this._showGutterMenu = observableConfigValue(InlineChatConfigKeys.ShowGutterMenu, false, this._configurationService); + this._renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this._configurationService); - this._gutterIndicator = this._store.add(this._instaService.createInstance(InlineChatSelectionIndicator, this._editor)); + this._gutterIndicator = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor)); this._zone = new Lazy(() => { @@ -285,14 +285,14 @@ export class InlineChatController implements IEditorContribution { // HIDE/SHOW const session = visibleSessionObs.read(r); - const showGutterMenu = this._showGutterMenu.read(r); + const renderMode = this._renderMode.read(r); if (!session) { this._zone.rawValue?.hide(); this._zone.rawValue?.widget.chatWidget.setModel(undefined); _editor.focus(); ctxInlineChatVisible.reset(); - } else if (showGutterMenu) { - // showGutterMenu mode: set model but don't show zone, keep focus in editor + } else if (renderMode === 'hover') { + // hover mode: set model but don't show zone, keep focus in editor this._zone.value.widget.chatWidget.setModel(session.chatModel); this._zone.rawValue?.hide(); ctxInlineChatVisible.set(true); @@ -438,16 +438,10 @@ export class InlineChatController implements IEditorContribution { existingSession.dispose(); } - // use gutter menu to ask for input - if (!arg?.message && this._configurationService.getValue(InlineChatConfigKeys.ShowGutterMenu)) { - const position = this._editor.getPosition(); - const editorDomNode = this._editor.getDomNode(); - const scrolledPosition = this._editor.getScrolledVisiblePosition(position); - const editorRect = editorDomNode.getBoundingClientRect(); - const x = editorRect.left + scrolledPosition.left; - const y = editorRect.top + scrolledPosition.top; + // use hover overlay to ask for input + if (!arg?.message && this._configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'hover') { // show menu and RETURN because the menu is re-entrant - await this._gutterIndicator.showMenuAt(x, y, scrolledPosition.height); + await this._gutterIndicator.showMenuAtSelection(); return true; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts new file mode 100644 index 00000000000..9fa3043f0fb --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/inlineChatEditorAffordance.css'; +import * as dom from '../../../../base/browser/dom.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; +import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { autorun, IObservable, ISettableObservable } from '../../../../base/common/observable.js'; +import { assertType } from '../../../../base/common/types.js'; + +/** + * Content widget that shows a small sparkle icon at the cursor position. + * When clicked, it shows the overlay widget for inline chat. + */ +export class InlineChatEditorAffordance extends Disposable implements IContentWidget { + + private static _idPool = 0; + + private readonly _id = `inline-chat-content-widget-${InlineChatEditorAffordance._idPool++}`; + private readonly _domNode: HTMLElement; + private _position: IContentWidgetPosition | null = null; + private _isVisible = false; + + readonly allowEditorOverflow = true; + readonly suppressMouseDown = false; + + constructor( + private readonly _editor: ICodeEditor, + selection: IObservable, + suppressAffordance: ISettableObservable, + private readonly _hover: ISettableObservable<{ rect: DOMRect; above: boolean } | undefined> + ) { + super(); + + // Create the widget DOM + this._domNode = dom.$('.inline-chat-content-widget'); + + // Add sparkle icon + const icon = dom.append(this._domNode, dom.$('.icon')); + icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.sparkle)); + + // Handle click to show overlay widget + this._store.add(dom.addDisposableListener(this._domNode, dom.EventType.CLICK, (e) => { + e.preventDefault(); + e.stopPropagation(); + this._showOverlayWidget(); + })); + + this._store.add(autorun(r => { + const sel = selection.read(r); + const suppressed = suppressAffordance.read(r); + if (sel && !suppressed) { + this._show(sel); + } else { + this._hide(); + } + })); + } + + private _show(selection: Selection): void { + + // Position at the cursor (active end of selection) + const cursorPosition = selection.getPosition(); + const direction = selection.getDirection(); + + // Show above for RTL (selection going up), below for LTR (selection going down) + const preference = direction === SelectionDirection.RTL + ? ContentWidgetPositionPreference.ABOVE + : ContentWidgetPositionPreference.BELOW; + + this._position = { + position: cursorPosition, + preference: [preference], + }; + + if (this._isVisible) { + this._editor.layoutContentWidget(this); + } else { + this._editor.addContentWidget(this); + this._isVisible = true; + } + } + + private _hide(): void { + if (this._isVisible) { + this._isVisible = false; + this._editor.removeContentWidget(this); + } + } + + private _showOverlayWidget(): void { + assertType(this._editor.hasModel()); + + if (!this._position || !this._position.position) { + return; + } + + const position = this._position.position; + const editorDomNode = this._editor.getDomNode(); + const scrolledPosition = this._editor.getScrolledVisiblePosition(position); + const editorRect = editorDomNode.getBoundingClientRect(); + const x = editorRect.left + scrolledPosition.left; + const y = editorRect.top + scrolledPosition.top; + + this._hide(); + this._hover.set({ + rect: new DOMRect(x, y, 0, scrolledPosition.height), + above: this._position.preference[0] === ContentWidgetPositionPreference.ABOVE + }, undefined); + } + + getId(): string { + return this._id; + } + + getDomNode(): HTMLElement { + return this._domNode; + } + + getPosition(): IContentWidgetPosition | null { + return this._position; + } + + override dispose(): void { + if (this._isVisible) { + this._editor.removeContentWidget(this); + } + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts new file mode 100644 index 00000000000..b1bff7c10d5 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../base/common/codicons.js'; +import { autorun, constObservable, derived, IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; +import { ObservableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js'; +import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { InlineEditsGutterIndicator, InlineEditsGutterIndicatorData, InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.js'; +import { InlineEditTabAction } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.js'; +import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { HoverService } from '../../../../platform/hover/browser/hoverService.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { CTX_INLINE_CHAT_GUTTER_VISIBLE } from '../common/inlineChat.js'; + +export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { + + + constructor( + private readonly _myEditorObs: ObservableCodeEditor, + selection: IObservable, + suppressAffordance: ISettableObservable, + private readonly _hover: ISettableObservable<{ rect: DOMRect; above: boolean } | undefined>, + @IHoverService hoverService: HoverService, + @IInstantiationService instantiationService: IInstantiationService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @IThemeService themeService: IThemeService, + @IContextKeyService contextKeyService: IContextKeyService, + ) { + const data = derived(r => { + const value = selection.read(r); + if (!value || suppressAffordance.read(r)) { + return undefined; + } + + // Use the cursor position (active end of selection) to determine the line + const cursorPosition = value.getPosition(); + const lineRange = new LineRange(cursorPosition.lineNumber, cursorPosition.lineNumber + 1); + + // Create minimal gutter menu data (empty for prototype) + const gutterMenuData = new InlineSuggestionGutterMenuData( + undefined, // action + '', // displayName + [], // extensionCommands + undefined, // alternativeAction + undefined, // modelInfo + undefined, // setModelId + ); + + return new InlineEditsGutterIndicatorData( + gutterMenuData, + lineRange, + new SimpleInlineSuggestModel(() => { }, () => { }), + undefined, // altAction + { + icon: Codicon.sparkle, + } + ); + }); + + const focusIsInMenu = observableValue({}, false); + + super( + _myEditorObs, data, constObservable(InlineEditTabAction.Jump), constObservable(0), constObservable(false), focusIsInMenu, + hoverService, instantiationService, accessibilityService, themeService + ); + + this._store.add(autorun(r => { + const element = _hover.read(r); + this._hoverVisible.set(!!element, undefined); + })); + + // Update context key when gutter visibility changes + const gutterVisibleCtxKey = CTX_INLINE_CHAT_GUTTER_VISIBLE.bindTo(contextKeyService); + this._store.add({ dispose: () => gutterVisibleCtxKey.reset() }); + this._store.add(autorun(reader => { + const isVisible = data.read(reader) !== undefined; + gutterVisibleCtxKey.set(isVisible); + })); + } + + protected override _showHover(): void { + + if (this._hoverVisible.get()) { + return; + } + + // Use the icon element from the base class as anchor + const iconElement = this._iconRef.element; + if (!iconElement) { + this._hover.set(undefined, undefined); + return; + } + + const selection = this._myEditorObs.cursorSelection.get(); + const direction = selection?.getDirection() ?? SelectionDirection.LTR; + this._hover.set({ rect: iconElement.getBoundingClientRect(), above: direction === SelectionDirection.RTL }, undefined); + } + + +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts new file mode 100644 index 00000000000..120a6d8a26a --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -0,0 +1,264 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { IAction } from '../../../../base/common/actions.js'; +import { ActionBar, ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { KeyCode } from '../../../../base/common/keyCodes.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { IActiveCodeEditor, ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; +import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; +import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { localize } from '../../../../nls.js'; +import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; +import { ACTION_START } from '../common/inlineChat.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js'; +import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js'; +import { InlineChatRunOptions } from './inlineChatController.js'; +import { Position } from '../../../../editor/common/core/position.js'; + +/** + * Overlay widget that displays a vertical action bar menu. + */ +export class InlineChatOverlayWidget extends Disposable implements IOverlayWidget { + + private static _idPool = 0; + + private readonly _id = `inline-chat-gutter-menu-${InlineChatOverlayWidget._idPool++}`; + private readonly _domNode: HTMLElement; + private readonly _inputContainer: HTMLElement; + private readonly _actionBar: ActionBar; + private readonly _input: IActiveCodeEditor; + private _position: IOverlayWidgetPosition | null = null; + private readonly _onDidHide = this._store.add(new Emitter()); + readonly onDidHide = this._onDidHide.event; + + private _isVisible = false; + private _inlineStartAction: IAction | undefined; + + readonly allowEditorOverflow = true; + + constructor( + private readonly _editor: ICodeEditor, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IMenuService private readonly _menuService: IMenuService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IInstantiationService instantiationService: IInstantiationService, + @IModelService modelService: IModelService, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(); + + // Create container + this._domNode = dom.$('.inline-chat-gutter-menu'); + + // Create input editor container + this._inputContainer = dom.append(this._domNode, dom.$('.input')); + this._inputContainer.style.width = '200px'; + this._inputContainer.style.height = '26px'; + this._inputContainer.style.display = 'flex'; + this._inputContainer.style.alignItems = 'center'; + this._inputContainer.style.justifyContent = 'center'; + + // Create editor options + const options = getSimpleEditorOptions(configurationService); + options.wordWrap = 'off'; + options.lineNumbers = 'off'; + options.glyphMargin = false; + options.lineDecorationsWidth = 0; + options.lineNumbersMinChars = 0; + options.folding = false; + options.minimap = { enabled: false }; + options.scrollbar = { vertical: 'auto', horizontal: 'hidden', alwaysConsumeMouseWheel: true, verticalSliderSize: 6 }; + options.renderLineHighlight = 'none'; + options.placeholder = this._keybindingService.appendKeybinding(localize('placeholderWithSelection', "Edit selection"), ACTION_START); + + const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { + isSimpleWidget: true, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + PlaceholderTextContribution.ID, + ]) + }; + + this._input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this._inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor; + + const model = this._store.add(modelService.createModel('', null, URI.parse(`gutter-input:${Date.now()}`), true)); + this._input.setModel(model); + this._input.layout({ width: 200, height: 18 }); + + // Listen to content size changes and resize the input editor (max 3 lines) + this._store.add(this._input.onDidContentSizeChange(e => { + if (e.contentHeightChanged) { + this._updateInputHeight(e.contentHeight); + } + })); + + // Handle Enter key to submit and ArrowDown to focus action bar + this._store.add(this._input.onKeyDown(e => { + if (e.keyCode === KeyCode.Enter && !e.shiftKey) { + const value = this._input.getModel().getValue() ?? ''; + // TODO@jrieken this isn't nice + if (this._inlineStartAction && value) { + e.preventDefault(); + e.stopPropagation(); + this._actionBar.actionRunner.run( + this._inlineStartAction, + { message: value, autoSend: true } satisfies InlineChatRunOptions + ); + } + } else if (e.keyCode === KeyCode.Escape) { + // Hide overlay if input is empty + const value = this._input.getModel().getValue() ?? ''; + if (!value) { + e.preventDefault(); + e.stopPropagation(); + this.hide(); + } + } else if (e.keyCode === KeyCode.DownArrow) { + // Focus first action bar item when at the end of the input + const inputModel = this._input.getModel(); + const position = this._input.getPosition(); + const lastLineNumber = inputModel.getLineCount(); + const lastLineMaxColumn = inputModel.getLineMaxColumn(lastLineNumber); + if (Position.equals(position, new Position(lastLineNumber, lastLineMaxColumn))) { + e.preventDefault(); + e.stopPropagation(); + this._actionBar.focus(); + } + } + })); + + // Create vertical action bar + this._actionBar = this._store.add(new ActionBar(this._domNode, { + orientation: ActionsOrientation.VERTICAL, + preventLoopNavigation: true, + })); + + // Track focus - hide when focus leaves + const focusTracker = this._store.add(dom.trackFocus(this._domNode)); + this._store.add(focusTracker.onDidBlur(() => this.hide())); + + // Handle action bar cancel (Escape key) + this._store.add(this._actionBar.onDidCancel(() => this.hide())); + this._store.add(this._actionBar.onWillRun(() => this.hide())); + } + + /** + * Show the widget at the specified position. + * @param top Top offset relative to editor + * @param left Left offset relative to editor + * @param anchorAbove Whether to anchor above the position (widget grows upward) + */ + show(top: number, left: number, anchorAbove: boolean): void { + + // Clear input state + this._input.getModel().setValue(''); + this._inputContainer.style.height = '26px'; + this._input.layout({ width: 200, height: 18 }); + + // Refresh actions from menu + this._refreshActions(); + + // Set initial position + this._position = { + preference: { top, left }, + stackOrdinal: 10000, + }; + + // Add widget to editor + if (!this._isVisible) { + this._editor.addOverlayWidget(this); + this._isVisible = true; + + } else if (!anchorAbove) { + this._editor.layoutOverlayWidget(this); + } + + // If anchoring above, adjust position after render to account for widget height + if (anchorAbove) { + const widgetHeight = this._domNode.offsetHeight; + this._position = { + preference: { top: top - widgetHeight, left }, + stackOrdinal: 10000, + }; + this._editor.layoutOverlayWidget(this); + } + + // Focus the input editor + setTimeout(() => this._input.focus(), 0); + } + + /** + * Hide the widget (removes from editor but does not dispose). + */ + hide(): void { + if (!this._isVisible) { + return; + } + this._isVisible = false; + this._editor.removeOverlayWidget(this); + this._onDidHide.fire(); + } + + private _refreshActions(): void { + // Clear existing actions + this._actionBar.clear(); + this._inlineStartAction = undefined; + + // Get fresh actions from menu + const actions = getFlatActionBarActions(this._menuService.getMenuActions(MenuId.ChatEditorInlineGutter, this._contextKeyService, { shouldForwardArgs: true })); + + // Set actions with keybindings (skip ACTION_START since we have the input editor) + for (const action of actions) { + if (action.id === ACTION_START) { + this._inlineStartAction = action; + continue; + } + const keybinding = this._keybindingService.lookupKeybinding(action.id)?.getLabel(); + this._actionBar.push(action, { icon: false, label: true, keybinding }); + } + } + + private _updateInputHeight(contentHeight: number): void { + const lineHeight = this._input.getOption(EditorOption.lineHeight); + const maxHeight = 3 * lineHeight; + const clampedHeight = Math.min(contentHeight, maxHeight); + const containerPadding = 8; + + this._inputContainer.style.height = `${clampedHeight + containerPadding}px`; + this._input.layout({ width: 200, height: clampedHeight }); + if (this._isVisible) { + this._editor.layoutOverlayWidget(this); + } + } + + getId(): string { + return this._id; + } + + getDomNode(): HTMLElement { + return this._domNode; + } + + getPosition(): IOverlayWidgetPosition | null { + return this._position; + } + + override dispose(): void { + if (this._isVisible) { + this._editor.removeOverlayWidget(this); + } + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSelectionGutterIndicator.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSelectionGutterIndicator.ts deleted file mode 100644 index eebb0032be9..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSelectionGutterIndicator.ts +++ /dev/null @@ -1,456 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../base/browser/dom.js'; -import { IAction } from '../../../../base/common/actions.js'; -import { ActionBar, ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { KeyCode } from '../../../../base/common/keyCodes.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { autorun, constObservable, debouncedObservable, derived, IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; -import { URI } from '../../../../base/common/uri.js'; -import { Emitter } from '../../../../base/common/event.js'; -import { IActiveCodeEditor, ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; -import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; -import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; -import { observableCodeEditor, ObservableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; -import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js'; -import { SelectionDirection } from '../../../../editor/common/core/selection.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; -import { InlineEditTabAction } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.js'; -import { InlineEditsGutterIndicator, InlineEditsGutterIndicatorData, InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { HoverService } from '../../../../platform/hover/browser/hoverService.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { localize } from '../../../../nls.js'; -import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; -import { ACTION_START, CTX_INLINE_CHAT_GUTTER_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js'; -import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js'; -import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; -import { InlineChatRunOptions } from './inlineChatController.js'; -import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; -import { Position } from '../../../../editor/common/core/position.js'; - - -export class InlineChatSelectionIndicator extends Disposable { - - private readonly _gutterIndicator: InlineChatGutterIndicator; - - constructor( - private readonly _editor: ICodeEditor, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService, - @IChatEntitlementService chatEntiteldService: IChatEntitlementService, - @IContextKeyService contextKeyService: IContextKeyService - ) { - super(); - - const enabled = observableConfigValue(InlineChatConfigKeys.ShowGutterMenu, false, configurationService); - - const editorObs = observableCodeEditor(this._editor); - const focusIsInMenu = observableValue(this, false); - - // Observable to suppress the gutter when an action is selected - const suppressGutter = observableValue(this, false); - - // Debounce the selection to add a delay before showing the indicator - const debouncedSelection = debouncedObservable(editorObs.cursorSelection, 500); - - // Context key for gutter visibility - const gutterVisibleCtxKey = CTX_INLINE_CHAT_GUTTER_VISIBLE.bindTo(contextKeyService); - this._store.add({ dispose: () => gutterVisibleCtxKey.reset() }); - - // Create data observable based on the primary selection - // Use raw selection for immediate hide, debounced for delayed show - const data = derived(reader => { - // Check if feature is enabled or if AI features are disabled - if (!enabled.read(reader) || chatEntiteldService.sentiment.hidden) { - return undefined; - } - - // Hide when suppressed (e.g., after an action is selected) - if (suppressGutter.read(reader)) { - return undefined; - } - - // Read raw selection - if empty, immediately hide - const rawSelection = editorObs.cursorSelection.read(reader); - if (!rawSelection || rawSelection.isEmpty()) { - return undefined; - } - - // Read debounced selection for showing - this adds delay - const selection = debouncedSelection.read(reader); - if (!selection || selection.isEmpty()) { - return undefined; - } - - // Use the cursor position (active end of selection) to determine the line - const cursorPosition = selection.getPosition(); - const lineRange = new LineRange(cursorPosition.lineNumber, cursorPosition.lineNumber + 1); - - // Create minimal gutter menu data (empty for prototype) - const gutterMenuData = new InlineSuggestionGutterMenuData( - undefined, // action - '', // displayName - [], // extensionCommands - undefined, // alternativeAction - undefined, // modelInfo - undefined, // setModelId - ); - - // Create model with console.log actions for prototyping - const model = new SimpleInlineSuggestModel(() => { }, () => { }); - - return new InlineEditsGutterIndicatorData( - gutterMenuData, - lineRange, - model, - undefined, // altAction - { - icon: Codicon.sparkle, - } - ); - }); - - // Instantiate the gutter indicator - this._gutterIndicator = this._store.add(this._instantiationService.createInstance( - InlineChatGutterIndicator, - editorObs, - data, - constObservable(InlineEditTabAction.Jump), // tabAction - not used with custom styles - constObservable(0), // verticalOffset - constObservable(false), // isHoveringOverInlineEdit - focusIsInMenu, - suppressGutter, - )); - - // Reset suppressGutter when the selection changes - this._store.add(autorun(reader => { - editorObs.cursorSelection.read(reader); - suppressGutter.set(false, undefined); - })); - - // Update context key when gutter visibility changes - this._store.add(autorun(reader => { - const isVisible = data.read(reader) !== undefined; - gutterVisibleCtxKey.set(isVisible); - })); - } - - /** - * Show the gutter menu at the specified coordinates. - * @returns Promise that resolves when menu closes - */ - showMenuAt(x: number, y: number, height: number = 0): Promise { - return this._gutterIndicator.showMenuAt(x, y, height); - } -} - -/** - * Overlay widget that displays a vertical action bar menu. - */ -class InlineChatGutterMenuWidget extends Disposable implements IOverlayWidget { - - private static _idPool = 0; - - private readonly _id = `inline-chat-gutter-menu-${InlineChatGutterMenuWidget._idPool++}`; - private readonly _domNode: HTMLElement; - private readonly _inputContainer: HTMLElement; - private readonly _actionBar: ActionBar; - private readonly _input: IActiveCodeEditor; - private _position: IOverlayWidgetPosition | null = null; - private readonly _onDidHide = this._register(new Emitter()); - readonly onDidHide = this._onDidHide.event; - - readonly allowEditorOverflow = true; - - constructor( - private readonly _editor: ICodeEditor, - top: number, - left: number, - anchorAbove: boolean, - @IKeybindingService keybindingService: IKeybindingService, - @IMenuService menuService: IMenuService, - @IContextKeyService contextKeyService: IContextKeyService, - @IInstantiationService instantiationService: IInstantiationService, - @IModelService modelService: IModelService, - @IConfigurationService configurationService: IConfigurationService, - ) { - super(); - - // Create container - this._domNode = dom.$('.inline-chat-gutter-menu'); - - // Create input editor container - this._inputContainer = dom.append(this._domNode, dom.$('.input')); - this._inputContainer.style.width = '200px'; - this._inputContainer.style.height = '26px'; - this._inputContainer.style.display = 'flex'; - this._inputContainer.style.alignItems = 'center'; - this._inputContainer.style.justifyContent = 'center'; - - // Create editor options - const options = getSimpleEditorOptions(configurationService); - options.wordWrap = 'off'; - options.lineNumbers = 'off'; - options.glyphMargin = false; - options.lineDecorationsWidth = 0; - options.lineNumbersMinChars = 0; - options.folding = false; - options.minimap = { enabled: false }; - options.scrollbar = { vertical: 'auto', horizontal: 'hidden', alwaysConsumeMouseWheel: true, verticalSliderSize: 6 }; - options.renderLineHighlight = 'none'; - options.placeholder = keybindingService.appendKeybinding(localize('placeholderWithSelection', "Edit selection"), ACTION_START); - - const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { - isSimpleWidget: true, - contributions: EditorExtensionsRegistry.getSomeEditorContributions([ - PlaceholderTextContribution.ID, - ]) - }; - - this._input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this._inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor; - - const model = this._store.add(modelService.createModel('', null, URI.parse(`gutter-input:${Date.now()}`), true)); - this._input.setModel(model); - this._input.layout({ width: 200, height: 18 }); - - // Listen to content size changes and resize the input editor (max 3 lines) - this._store.add(this._input.onDidContentSizeChange(e => { - if (e.contentHeightChanged) { - this._updateInputHeight(e.contentHeight); - } - })); - - let inlineStartAction: IAction | undefined; - - // Handle Enter key to submit and ArrowDown to focus action bar - this._store.add(this._input.onKeyDown(e => { - if (e.keyCode === KeyCode.Enter && !e.shiftKey) { - const value = this._input.getModel().getValue() ?? ''; - // TODO@jrieken this isn't nice - if (inlineStartAction && value) { - e.preventDefault(); - e.stopPropagation(); - this._actionBar.actionRunner.run( - inlineStartAction, - { message: value, autoSend: true } satisfies InlineChatRunOptions - ); - } - } else if (e.keyCode === KeyCode.DownArrow) { - // Focus first action bar item when at the end of the input - const inputModel = this._input.getModel(); - const position = this._input.getPosition(); - const lastLineNumber = inputModel.getLineCount(); - const lastLineMaxColumn = inputModel.getLineMaxColumn(lastLineNumber); - if (Position.equals(position, new Position(lastLineNumber, lastLineMaxColumn))) { - e.preventDefault(); - e.stopPropagation(); - this._actionBar.focus(); - } - } - })); - - // Get actions from menu - const actions = getFlatActionBarActions(menuService.getMenuActions(MenuId.ChatEditorInlineGutter, contextKeyService, { shouldForwardArgs: true })); - - // Create vertical action bar - this._actionBar = this._store.add(new ActionBar(this._domNode, { - orientation: ActionsOrientation.VERTICAL, - })); - - // Set actions with keybindings (skip ACTION_START since we have the input editor) - for (const action of actions) { - if (action.id === ACTION_START) { - inlineStartAction = action; - continue; - } - const keybinding = keybindingService.lookupKeybinding(action.id)?.getLabel(); - this._actionBar.push(action, { icon: false, label: true, keybinding }); - } - - // Set initial position - this._position = { - preference: { top, left }, - stackOrdinal: 10000, - }; - - // Track focus - hide when focus leaves - const focusTracker = this._store.add(dom.trackFocus(this._domNode)); - this._store.add(focusTracker.onDidBlur(() => this._hide())); - - // Handle action bar cancel (Escape key) - this._store.add(this._actionBar.onDidCancel(() => this._hide())); - this._store.add(this._actionBar.onWillRun(() => this._hide())); - - // Add widget to editor - this._editor.addOverlayWidget(this); - - // If anchoring above, adjust position after render to account for widget height - if (anchorAbove) { - const widgetHeight = this._domNode.offsetHeight; - this._position = { - preference: { top: top - widgetHeight, left }, - stackOrdinal: 10000, - }; - this._editor.layoutOverlayWidget(this); - } - - // Focus the input editor - setTimeout(() => this._input.focus(), 0); - } - - private _hide(): void { - this._onDidHide.fire(); - } - - private _updateInputHeight(contentHeight: number): void { - const lineHeight = this._input.getOption(EditorOption.lineHeight); - const maxHeight = 3 * lineHeight; - const clampedHeight = Math.min(contentHeight, maxHeight); - const containerPadding = 8; - - this._inputContainer.style.height = `${clampedHeight + containerPadding}px`; - this._input.layout({ width: 200, height: clampedHeight }); - this._editor.layoutOverlayWidget(this); - } - - getId(): string { - return this._id; - } - - getDomNode(): HTMLElement { - return this._domNode; - } - - getPosition(): IOverlayWidgetPosition | null { - return this._position; - } - - override dispose(): void { - this._editor.removeOverlayWidget(this); - super.dispose(); - } -} - -/** - * Custom gutter indicator for selection that shows a menu overlay widget. - */ -class InlineChatGutterIndicator extends InlineEditsGutterIndicator { - - private readonly _myInstantiationService: IInstantiationService; - private _currentMenuWidget: InlineChatGutterMenuWidget | undefined; - - constructor( - private readonly _myEditorObs: ObservableCodeEditor, - data: IObservable, - tabAction: IObservable, - verticalOffset: IObservable, - isHoveringOverInlineEdit: IObservable, - focusIsInMenu: ISettableObservable, - private readonly _suppressGutter: ISettableObservable, - @IHoverService hoverService: HoverService, - @IInstantiationService instantiationService: IInstantiationService, - @IAccessibilityService accessibilityService: IAccessibilityService, - @IThemeService themeService: IThemeService, - ) { - super(_myEditorObs, data, tabAction, verticalOffset, isHoveringOverInlineEdit, focusIsInMenu, hoverService, instantiationService, accessibilityService, themeService); - this._myInstantiationService = instantiationService; - } - - protected override _showHover(): void { - - if (this._hoverVisible.get()) { - return; - } - - // Use the icon element from the base class as anchor - const iconElement = this._iconRef.element; - if (!iconElement) { - return; - } - - this._hoverVisible.set(true, undefined); - const rect = iconElement.getBoundingClientRect(); - - this.showMenuAt(rect.left, rect.top, rect.height).finally(() => { - this._hoverVisible.set(false, undefined); - }); - } - - /** - * Show the gutter menu at the specified coordinates. - * @returns Promise that resolves when menu closes - */ - showMenuAt(x: number, y: number, height: number = 0): Promise { - return new Promise(resolve => { - // Clean up existing widget if any - this._currentMenuWidget?.dispose(); - this._currentMenuWidget = undefined; - - // Determine selection direction to position menu above or below - const selection = this._myEditorObs.cursorSelection.get(); - const direction = selection?.getDirection() ?? SelectionDirection.LTR; - - // Convert screen coordinates to editor-relative coordinates - const editor = this._myEditorObs.editor; - const editorDomNode = editor.getDomNode(); - if (!editorDomNode) { - resolve(); - return; - } - - const editorRect = editorDomNode.getBoundingClientRect(); - const padding = 1; - - // Calculate position relative to editor - // For RTL (above), we pass the top of the gutter indicator; widget will adjust after measuring its height - // For LTR (below), we pass the bottom of the gutter indicator - const anchorAbove = direction === SelectionDirection.RTL; - let top: number; - if (anchorAbove) { - // Pass the top of the gutter indicator minus padding - top = y - editorRect.top - padding; - } else { - // Menu appears below - position at bottom of gutter indicator - top = y - editorRect.top + height + padding; - } - const left = x - editorRect.left; - - const store = new DisposableStore(); - - // Create and show overlay widget - this._currentMenuWidget = this._myInstantiationService.createInstance( - InlineChatGutterMenuWidget, - editor, - top, - left, - anchorAbove, - ); - - // Handle widget hide - store.add(this._currentMenuWidget.onDidHide(() => { - this._suppressGutter.set(true, undefined); - store.dispose(); - this._currentMenuWidget?.dispose(); - this._currentMenuWidget = undefined; - - // Focus editor - editor.focus(); - - resolve(); - })); - }); - } -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css new file mode 100644 index 00000000000..81a039d9d5c --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.inline-chat-content-widget { + box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); + border: 1px solid var(--vscode-widget-border, transparent); + border-radius: 4px; + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-background); + display: flex; + height: 16px; + width: 16px; + align-items: center; + justify-content: center; + cursor: pointer; + padding: 2px; +} + +.inline-chat-content-widget:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.inline-chat-content-widget .icon.codicon { + margin: 0; + color: var(--vscode-button-foreground); +} diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index e3a74c6adb6..311cb19ddda 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -15,13 +15,13 @@ import { NOTEBOOK_IS_ACTIVE_EDITOR } from '../../notebook/common/notebookContext export const enum InlineChatConfigKeys { FinishOnType = 'inlineChat.finishOnType', - StartWithOverlayWidget = 'inlineChat.startWithOverlayWidget', HoldToSpeech = 'inlineChat.holdToSpeech', /** @deprecated do not read on client */ EnableV2 = 'inlineChat.enableV2', notebookAgent = 'inlineChat.notebookAgent', PersistModelChoice = 'inlineChat.persistModelChoice', - ShowGutterMenu = 'inlineChat.showGutterMenu', + Affordance = 'inlineChat.affordance', + RenderMode = 'inlineChat.renderMode', } Registry.as(Extensions.Configuration).registerConfiguration({ @@ -63,10 +63,27 @@ Registry.as(Extensions.Configuration).registerConfigurat mode: 'auto' } }, - [InlineChatConfigKeys.ShowGutterMenu]: { - description: localize('showGutterMenu', "Controls whether a gutter indicator is shown when text is selected to quickly access inline chat."), - default: false, - type: 'boolean', + [InlineChatConfigKeys.Affordance]: { + description: localize('affordance', "Controls whether an inline chat affordance is shown when text is selected."), + default: 'off', + type: 'string', + enum: ['off', 'gutter', 'editor'], + enumDescriptions: [ + localize('affordance.off', "No affordance is shown."), + localize('affordance.gutter', "Show an affordance in the gutter."), + localize('affordance.editor', "Show an affordance in the editor at the cursor position."), + ], + tags: ['experimental'] + }, + [InlineChatConfigKeys.RenderMode]: { + description: localize('renderMode', "Controls how inline chat is rendered."), + default: 'zone', + type: 'string', + enum: ['zone', 'hover'], + enumDescriptions: [ + localize('renderMode.zone', "Render inline chat as a zone widget below the current line."), + localize('renderMode.hover', "Render inline chat as a hover overlay."), + ], tags: ['experimental'] } }