diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index ce4653f5cfb..f99d40fbd0f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -6,10 +6,10 @@ import './inlineChatDefaultModel.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; -import { IMenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { IMenuItem, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { InlineChatController } from './inlineChatController.js'; import * as InlineChatActions from './inlineChatActions.js'; -import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, MENU_INLINE_CHAT_WIDGET_STATUS, ACTION_START } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; @@ -23,8 +23,6 @@ import { localize } from '../../../../nls.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { InlineChatAccessibilityHelp } from './inlineChatAccessibilityHelp.js'; -import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; -import { Codicon } from '../../../../base/common/codicons.js'; registerEditorContribution(InlineChatController.ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors @@ -86,25 +84,17 @@ const cancelActionMenuItem: IMenuItem = { MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, cancelActionMenuItem); -// --- InlineChatEditorAffordance menu --- -MenuRegistry.appendMenuItem(MenuId.InlineChatEditorAffordance, { - group: '0_chat', - order: 1, - command: { - id: ACTION_START, - title: localize('editCode', "Ask for Edits"), - shortTitle: localize('editCodeShort', "Ask for Edits"), - icon: Codicon.sparkle, - }, - when: EditorContextKeys.hasNonEmptySelection, -}); // --- actions --- registerAction2(InlineChatActions.StartSessionAction); +registerAction2(InlineChatActions.AskInChatAction); registerAction2(InlineChatActions.FocusInlineChat); registerAction2(InlineChatActions.SubmitInlineChatInputAction); +registerAction2(InlineChatActions.SubmitToChatAction); +registerAction2(InlineChatActions.AttachToChatAction); +registerAction2(InlineChatActions.HideInlineChatInputAction); const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 1b1a6396311..39bbc646a00 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,10 +11,10 @@ import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diff import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { InlineChatController, InlineChatRunOptions } from './inlineChatController.js'; -import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_V2_ENABLED, CTX_INLINE_CHAT_V1_ENABLED, CTX_HOVER_MODE, CTX_INLINE_CHAT_INPUT_HAS_TEXT } from '../common/inlineChat.js'; +import { ACTION_ACCEPT_CHANGES, ACTION_ASK_IN_CHAT, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_V2_ENABLED, CTX_INLINE_CHAT_V1_ENABLED, CTX_HOVER_MODE, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, InlineChatConfigKeys } from '../common/inlineChat.js'; import { ctxHasEditorModification, ctxHasRequestInProgress } from '../../chat/browser/chatEditing/chatEditingEditorContextKeys.js'; import { localize, localize2 } from '../../../../nls.js'; -import { Action2, IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js'; +import { Action2, IAction2Options, MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; @@ -25,6 +25,13 @@ import { CommandsRegistry } from '../../../../platform/commands/common/commands. import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IChatEditingService } from '../../chat/common/editing/chatEditingService.js'; +import { IChatWidgetService } from '../../chat/browser/chat.js'; +import { IAgentFeedbackVariableEntry } from '../../chat/common/attachments/chatVariableEntries.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { basename } from '../../../../base/common/resources.js'; +import { ChatRequestQueueKind } from '../../chat/common/chatService/chatService.js'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); @@ -59,7 +66,7 @@ export class StartSessionAction extends Action2 { shortTitle: localize2('runShort', 'Inline Chat'), category: AbstractInlineChatAction.category, f1: true, - precondition: inlineChatContextKey, + precondition: ContextKeyExpr.and(inlineChatContextKey, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), keybinding: { when: EditorContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib, @@ -103,6 +110,8 @@ export class StartSessionAction extends Action2 { private async _runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]) { + const configServce = accessor.get(IConfigurationService); + const ctrl = InlineChatController.get(editor); if (!ctrl) { return; @@ -117,10 +126,36 @@ export class StartSessionAction extends Action2 { if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) { options = arg; } - await InlineChatController.get(editor)?.run({ ...options }); + + // use hover overlay to ask for input + if (!options?.message && configServce.getValue(InlineChatConfigKeys.RenderMode) === 'hover') { + const selection = editor.getSelection(); + const placeholder = selection && !selection.isEmpty() + ? localize('placeholderWithSelection', "Describe how to change this") + : localize('placeholderNoSelection', "Describe what to generate"); + // show menu and RETURN because the menu is re-entrant + await ctrl.inputOverlayWidget.showMenuAtSelection(placeholder); + return; + } + + await ctrl?.run({ ...options }); } } +// --- InlineChatEditorAffordance menu --- + +MenuRegistry.appendMenuItem(MenuId.InlineChatEditorAffordance, { + group: '0_chat', + order: 1, + when: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasNonEmptySelection, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + command: { + id: ACTION_START, + title: localize('editCode', "Ask for Edits"), + shortTitle: localize('editCodeShort', "Ask for Edits"), + icon: Codicon.sparkle, + } +}); + export class FocusInlineChat extends EditorAction2 { constructor() { @@ -334,11 +369,17 @@ export class SubmitInlineChatInputAction extends AbstractInlineChatAction { id: 'inlineChat.submitInput', title: localize2('submitInput', "Send"), icon: Codicon.send, - precondition: CTX_INLINE_CHAT_INPUT_HAS_TEXT, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + keybinding: { + when: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate()), + weight: KeybindingWeight.EditorCore + 10, + primary: KeyCode.Enter + }, menu: [{ id: MenuId.InlineChatInput, group: '0_main', order: 1, + when: CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.negate() }] }); } @@ -351,3 +392,206 @@ export class SubmitInlineChatInputAction extends AbstractInlineChatAction { } } } + +export class HideInlineChatInputAction extends AbstractInlineChatAction { + + constructor() { + super({ + id: 'inlineChat.hideInput', + title: localize2('hideInput', "Hide Input"), + precondition: CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, + keybinding: { + when: CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, + weight: KeybindingWeight.EditorCore + 10, + primary: KeyCode.Escape + } + }); + } + + override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: unknown[]): void { + ctrl.inputWidget.hide(); + } +} + + +export class AskInChatAction extends EditorAction2 { + + constructor() { + super({ + id: ACTION_ASK_IN_CHAT, + title: localize2('askInChat', 'Ask in Chat'), + category: AbstractInlineChatAction.category, + f1: true, + precondition: ContextKeyExpr.and(inlineChatContextKey, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + keybinding: { + when: EditorContextKeys.focus, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyI + }, + icon: Codicon.chatSparkle, + menu: [{ + id: MenuId.EditorContext, + group: '1_chat', + order: 3, + when: ContextKeyExpr.and(inlineChatContextKey, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT) + }, { + id: MenuId.InlineChatEditorAffordance, + group: '0_chat', + order: 1, + when: ContextKeyExpr.and(EditorContextKeys.hasNonEmptySelection, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT) + }] + }); + } + + override async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor) { + const chatEditingService = accessor.get(IChatEditingService); + const ctrl = InlineChatController.get(editor); + if (!ctrl || !editor.hasModel()) { + return; + } + const entry = chatEditingService.editingSessionsObs.get().find(value => value.getEntry(editor.getModel().uri)); + if (entry) { + ctrl.inputOverlayWidget.showMenuAtSelection(localize('placeholderAskInChat', "Describe how to proceed in Chat")); + } + } +} + +export class SubmitToChatAction extends AbstractInlineChatAction { + + + constructor() { + super({ + id: 'inlineChat.submitToChat', + title: localize2('submitToChat', "Send to Chat"), + icon: Codicon.arrowUp, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + keybinding: { + when: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + weight: KeybindingWeight.EditorCore + 10, + primary: KeyCode.Enter + }, + menu: [{ + id: MenuId.InlineChatInput, + group: '0_main', + order: 1, + when: CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, + alt: { + id: AttachToChatAction.Id, + title: localize2('attachToChat', "Attach to Chat"), + icon: Codicon.attach + } + }] + }); + } + + override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor): Promise { + const chatEditingService = accessor.get(IChatEditingService); + const chatWidgetService = accessor.get(IChatWidgetService); + if (!editor.hasModel()) { + return; + } + + const value = ctrl.inputWidget.value; + ctrl.inputWidget.hide(); + if (!value) { + return; + } + + const session = chatEditingService.editingSessionsObs.get().find(s => s.getEntry(editor.getModel().uri)); + if (!session) { + return; + } + + const widget = await chatWidgetService.openSession(session.chatSessionResource); + if (!widget) { + return; + } + + const selection = editor.getSelection(); + if (selection && !selection.isEmpty()) { + await widget.attachmentModel.addFile(editor.getModel().uri, selection); + } + await widget.acceptInput(value, { queue: ChatRequestQueueKind.Queued }); + } +} + +export class AttachToChatAction extends AbstractInlineChatAction { + + static readonly Id = 'inlineChat.attachToChat'; + + constructor() { + super({ + id: AttachToChatAction.Id, + title: localize2('attachToChat', "Attach to Chat"), + icon: Codicon.attach, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + keybinding: { + when: ContextKeyExpr.and(CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED, CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT), + weight: KeybindingWeight.EditorCore + 10, + primary: KeyMod.CtrlCmd | KeyCode.Enter, + secondary: [KeyMod.Alt | KeyCode.Enter] + }, + }); + } + + override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor): Promise { + const chatEditingService = accessor.get(IChatEditingService); + const chatWidgetService = accessor.get(IChatWidgetService); + if (!editor.hasModel()) { + return; + } + + const value = ctrl.inputWidget.value; + const selection = editor.getSelection(); + ctrl.inputWidget.hide(); + if (!value || !selection || selection.isEmpty()) { + return; + } + + const session = chatEditingService.editingSessionsObs.get().find(s => s.getEntry(editor.getModel().uri)); + if (!session) { + return; + } + + const widget = await chatWidgetService.openSession(session.chatSessionResource); + if (!widget) { + return; + } + + const uri = editor.getModel().uri; + const selectedText = editor.getModel().getValueInRange(selection); + const fileName = basename(uri); + const lineRef = selection.startLineNumber === selection.endLineNumber + ? `${selection.startLineNumber}` + : `${selection.startLineNumber}-${selection.endLineNumber}`; + + const feedbackValue = [ + ``, + ``, + selectedText, + ``, + ``, + value, + ``, + `` + ].join('\n'); + + const feedbackId = generateUuid(); + const entry: IAgentFeedbackVariableEntry = { + kind: 'agentFeedback', + id: `inlineChat.feedback.${feedbackId}`, + name: localize('attachToChat.name', "{0}:{1}", fileName, lineRef), + icon: Codicon.comment, + sessionResource: session.chatSessionResource, + feedbackItems: [{ + id: feedbackId, + text: value, + resourceUri: uri, + range: selection, + }], + value: feedbackValue, + }; + + widget.attachmentModel.addContext(entry); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts index ea0933b7e44..4e3cf4c6600 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -44,7 +44,7 @@ export class InlineChatAffordance extends Disposable { readonly #editor: ICodeEditor; readonly #inputWidget: InlineChatInputWidget; readonly #instantiationService: IInstantiationService; - readonly #menuData = observableValue<{ rect: DOMRect; above: boolean; lineNumber: number } | undefined>(this, undefined); + readonly #menuData = observableValue<{ rect: DOMRect; above: boolean; lineNumber: number; placeholder: string } | undefined>(this, undefined); constructor( editor: ICodeEditor, @@ -118,7 +118,6 @@ export class InlineChatAffordance extends Disposable { InlineChatGutterAffordance, editorObs, derived(r => affordance.read(r) === 'gutter' ? selectionData.read(r) : undefined), - this.#menuData )); const editorAffordance = this.#instantiationService.createInstance( @@ -157,7 +156,7 @@ export class InlineChatAffordance extends Disposable { const left = data.rect.left - editorRect.left; // Show the overlay widget - this.#inputWidget.show(data.lineNumber, left, data.above); + this.#inputWidget.show(data.lineNumber, left, data.above, data.placeholder); })); this._store.add(autorun(r => { @@ -168,7 +167,7 @@ export class InlineChatAffordance extends Disposable { })); } - async showMenuAtSelection() { + async showMenuAtSelection(placeholder: string): Promise { assertType(this.#editor.hasModel()); const direction = this.#editor.getSelection().getDirection(); @@ -182,7 +181,8 @@ export class InlineChatAffordance extends Disposable { this.#menuData.set({ rect: new DOMRect(x, y, 0, scrolledPosition.height), above: direction === SelectionDirection.RTL, - lineNumber: position.lineNumber + lineNumber: position.lineNumber, + placeholder }, undefined); await waitForState(this.#inputWidget.position, pos => pos === null); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 79335bf93cd..6415bfcfe49 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -39,7 +39,7 @@ import { ISharedWebContentExtractorService } from '../../../../platform/webConte import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; import { IChatAttachmentResolveService } from '../../chat/browser/attachments/chatAttachmentResolveService.js'; import { IChatWidgetLocationOptions } from '../../chat/browser/widget/chatWidget.js'; -import { ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js'; +import { IChatEditingService, ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js'; import { ChatModel } from '../../chat/common/model/chatModel.js'; import { ChatMode } from '../../chat/common/chatModes.js'; import { IChatService } from '../../chat/common/chatService/chatService.js'; @@ -51,14 +51,13 @@ import { isNotebookContainingCellEditor as isNotebookWithCellEditor } from '../. import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; 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 { CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT, CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; import { InlineChatAffordance } from './inlineChatAffordance.js'; import { InlineChatInputWidget, InlineChatSessionOverlayWidget } from './inlineChatOverlayWidget.js'; import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; - export abstract class InlineChatRunOptions { initialSelection?: ISelection; @@ -117,7 +116,7 @@ export class InlineChatController implements IEditorContribution { private readonly _isActiveController = observableValue(this, false); private readonly _renderMode: IObservable<'zone' | 'hover'>; private readonly _zone: Lazy; - private readonly _gutterIndicator: InlineChatAffordance; + readonly inputOverlayWidget: InlineChatAffordance; private readonly _inputWidget: InlineChatInputWidget; private readonly _currentSession: IObservable; @@ -149,17 +148,42 @@ export class InlineChatController implements IEditorContribution { @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, @ILanguageModelsService private readonly _languageModelService: ILanguageModelsService, @ILogService private readonly _logService: ILogService, + @IChatEditingService private readonly _chatEditingService: IChatEditingService, ) { const editorObs = observableCodeEditor(_editor); - const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); + const ctxFileBelongsToChat = CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.bindTo(contextKeyService); const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService); this._renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this._configurationService); + // Track whether the current editor's file is being edited by any chat editing session + this._store.add(autorun(r => { + const model = editorObs.model.read(r); + if (!model) { + ctxFileBelongsToChat.set(false); + return; + } + const sessions = this._chatEditingService.editingSessionsObs.read(r); + let hasEdits = false; + for (const session of sessions) { + const entries = session.entries.read(r); + for (const entry of entries) { + if (isEqual(entry.modifiedURI, model.uri)) { + hasEdits = true; + break; + } + } + if (hasEdits) { + break; + } + } + ctxFileBelongsToChat.set(hasEdits); + })); + const overlayWidget = this._inputWidget = this._store.add(this._instaService.createInstance(InlineChatInputWidget, editorObs)); const sessionOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatSessionOverlayWidget, editorObs)); - this._gutterIndicator = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget)); + this.inputOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget)); this._zone = new Lazy(() => { @@ -231,8 +255,6 @@ export class InlineChatController implements IEditorContribution { return result; }); - - const sessionsSignal = observableSignalFromEvent(this, _inlineChatSessionService.onDidChangeSessions); this._currentSession = derived(r => { @@ -474,18 +496,10 @@ export class InlineChatController implements IEditorContribution { existingSession.dispose(); } - // 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.showMenuAtSelection(); - return true; - } - this._isActiveController.set(true, undefined); const session = this._inlineChatSessionService.createSession(this._editor); - // Store for tracking model changes during this session const sessionStore = new DisposableStore(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts index 519ae158e7f..1123978c2e8 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts @@ -28,7 +28,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { ACTION_START } from '../common/inlineChat.js'; +import { ACTION_START, ACTION_ASK_IN_CHAT } from '../common/inlineChat.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; class QuickFixActionViewItem extends MenuEntryActionViewItem { @@ -105,7 +105,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem { } } -class InlineChatStartActionViewItem extends MenuEntryActionViewItem { +class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem { private readonly _kbLabel: string | undefined; @@ -150,7 +150,7 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi private readonly _onDidRunAction = this._store.add(new Emitter()); readonly onDidRunAction: Event = this._onDidRunAction.event; - readonly allowEditorOverflow = false; + readonly allowEditorOverflow = true; readonly suppressMouseDown = false; constructor( @@ -173,8 +173,8 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi if (action instanceof MenuItemAction && action.id === quickFixCommandId) { return instantiationService.createInstance(QuickFixActionViewItem, action, this._editor); } - if (action instanceof MenuItemAction && action.id === ACTION_START) { - return instantiationService.createInstance(InlineChatStartActionViewItem, action); + if (action instanceof MenuItemAction && (action.id === ACTION_START || action.id === ACTION_ASK_IN_CHAT)) { + return instantiationService.createInstance(LabelWithKeybindingActionViewItem, action); } return undefined; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts index 19d67a29e95..3d82cec90ec 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts @@ -5,11 +5,11 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { autorun, constObservable, derived, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; +import { constObservable, derived, IObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.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 { Selection } from '../../../../editor/common/core/selection.js'; import { InlineCompletionCommand } from '../../../../editor/common/languages.js'; import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js'; import { InlineEditsGutterIndicator, InlineEditsGutterIndicatorData, InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.js'; @@ -30,9 +30,8 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { readonly onDidRunAction: Event = this._onDidRunAction.event; constructor( - private readonly _myEditorObs: ObservableCodeEditor, + myEditorObs: ObservableCodeEditor, selection: IObservable, - private readonly _hover: ISettableObservable<{ rect: DOMRect; above: boolean; lineNumber: number } | undefined>, @IKeybindingService _keybindingService: IKeybindingService, @IHoverService hoverService: HoverService, @IInstantiationService instantiationService: IInstantiationService, @@ -46,7 +45,7 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { const menu = menuService.createMenu(MenuId.InlineChatEditorAffordance, contextKeyService); const menuObs = observableFromEvent(menu.onDidChange, () => menu.getActions({ renderShortTitle: false })); - const codeActionController = CodeActionController.get(_myEditorObs.editor); + const codeActionController = CodeActionController.get(myEditorObs.editor); const lightBulbObs = codeActionController?.lightBulbState; const data = derived(r => { @@ -93,7 +92,7 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { return new InlineEditsGutterIndicatorData( gutterMenuData, lineRange, - new SimpleInlineSuggestModel(() => { }, () => this._doShowHover()), + new SimpleInlineSuggestModel(() => { }, () => { }), undefined, // altAction { icon } ); @@ -102,36 +101,13 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { const focusIsInMenu = observableValue({}, false); super( - _myEditorObs, data, constObservable(InlineEditTabAction.Inactive), constObservable(0), constObservable(false), focusIsInMenu, + myEditorObs, data, constObservable(InlineEditTabAction.Inactive), constObservable(0), constObservable(false), focusIsInMenu, hoverService, instantiationService, accessibilityService, themeService, userInteractionService ); this._store.add(menu); - this._store.add(autorun(r => { - const element = _hover.read(r); - this._hoverVisible.set(!!element, undefined); - })); this._store.add(this.onDidCloseWithCommand(commandId => this._onDidRunAction.fire(commandId))); } - - private _doShowHover(): 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; - const lineNumber = selection?.getPosition().lineNumber ?? 1; - this._hover.set({ rect: iconElement.getBoundingClientRect(), above: direction === SelectionDirection.RTL, lineNumber }, undefined); - } - } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index 844b7dae003..1d27ddb7da5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -28,9 +28,8 @@ import { getFlatActionBarActions } from '../../../../platform/actions/browser/me import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ChatEditingAcceptRejectActionViewItem } from '../../chat/browser/chatEditing/chatEditingEditorOverlay.js'; -import { CTX_INLINE_CHAT_INPUT_HAS_TEXT } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_INPUT_HAS_TEXT, CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED } from '../common/inlineChat.js'; import { StickyScrollController } from '../../../../editor/contrib/stickyScroll/browser/stickyScrollController.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -53,7 +52,6 @@ export class InlineChatInputWidget extends Disposable { private readonly _position = observableValue(this, null); readonly position: IObservable = this._position; - private readonly _showStore = this._store.add(new DisposableStore()); private readonly _stickyScrollHeight: IObservable; private readonly _layoutData: IObservable<{ totalWidth: number; toolbarWidth: number; height: number; editorPad: number }>; @@ -65,7 +63,6 @@ export class InlineChatInputWidget extends Disposable { constructor( private readonly _editorObs: ObservableCodeEditor, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @ICommandService private readonly _commandService: ICommandService, @IMenuService private readonly _menuService: IMenuService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, @@ -153,24 +150,14 @@ export class InlineChatInputWidget extends Disposable { this._store.add(resizeObserver); this._store.add(resizeObserver.observe(toolbar.getElement())); - // Compute min and max widget width based on editor content width - const maxWidgetWidth = derived(r => { - const layoutInfo = this._editorObs.layoutInfo.read(r); - return Math.max(0, Math.round(layoutInfo.contentWidth * 0.70)); - }); - const minWidgetWidth = derived(r => { - const layoutInfo = this._editorObs.layoutInfo.read(r); - return Math.max(0, Math.round(layoutInfo.contentWidth * 0.33)); - }); - const contentWidth = observableFromEvent(this, this._input.onDidChangeModelContent, () => this._input.getContentWidth()); const contentHeight = observableFromEvent(this, this._input.onDidContentSizeChange, () => this._input.getContentHeight()); this._layoutData = derived(r => { const editorPad = 6; const totalWidth = contentWidth.read(r) + editorPad + toolbarWidth.read(r); - const minWidth = minWidgetWidth.read(r); - const maxWidth = maxWidgetWidth.read(r); + const minWidth = 220; + const maxWidth = 600; const clampedWidth = this._input.getOption(EditorOption.wordWrap) === 'on' ? maxWidth : Math.max(minWidth, Math.min(totalWidth, maxWidth)); @@ -210,17 +197,6 @@ export class InlineChatInputWidget extends Disposable { this._toolbarContainer.classList.toggle('fake-scroll-decoration', e.scrollTop > 0); })); - // Update placeholder based on selection state - this._store.add(autorun(r => { - const selection = this._editorObs.cursorSelection.read(r); - const hasSelection = selection && !selection.isEmpty(); - const placeholderText = hasSelection - ? localize('placeholderWithSelection', "Describe how to change this") - : localize('placeholderNoSelection', "Describe what to generate"); - - this._input.updateOptions({ placeholder: placeholderText }); - })); - // Track input text for context key and adjust width based on content const inputHasText = CTX_INLINE_CHAT_INPUT_HAS_TEXT.bindTo(this._contextKeyService); @@ -229,21 +205,15 @@ export class InlineChatInputWidget extends Disposable { })); this._store.add(toDisposable(() => inputHasText.reset())); - // Handle Enter key to submit, ArrowDown to move to actions + // Track focus state + const inputWidgetFocused = CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED.bindTo(this._contextKeyService); + this._store.add(this._input.onDidFocusEditorText(() => inputWidgetFocused.set(true))); + this._store.add(this._input.onDidBlurEditorText(() => inputWidgetFocused.set(false))); + this._store.add(toDisposable(() => inputWidgetFocused.reset())); + + // Handle key events: ArrowDown to move to actions this._store.add(this._input.onKeyDown(e => { - if (e.keyCode === KeyCode.Enter && !e.shiftKey) { - e.preventDefault(); - e.stopPropagation(); - this._commandService.executeCommand('inlineChat.submitInput'); - } 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 && !actionBar.isEmpty()) { + if (e.keyCode === KeyCode.DownArrow && !actionBar.isEmpty()) { const model = this._input.getModel(); const position = this._input.getPosition(); if (position && position.lineNumber === model.getLineCount()) { @@ -282,11 +252,11 @@ export class InlineChatInputWidget extends Disposable { * @param left Left offset relative to editor * @param anchorAbove Whether to anchor above the position (widget grows upward) */ - show(lineNumber: number, left: number, anchorAbove: boolean): void { + show(lineNumber: number, left: number, anchorAbove: boolean, placeholder: string): void { this._showStore.clear(); // Clear input state - this._input.updateOptions({ wordWrap: 'off' }); + this._input.updateOptions({ wordWrap: 'off', placeholder }); this._input.getModel().setValue(''); // Store anchor info for scroll updates diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css index b50a69f8752..37d4268342a 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css @@ -15,6 +15,7 @@ min-height: var(--vscode-inline-chat-affordance-height); line-height: var(--vscode-inline-chat-affordance-height); border: 1px solid var(--vscode-input-border, transparent); + z-index: 100; } .inline-chat-content-widget .action-label.codicon.codicon-light-bulb, diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css index 294867b3056..9acc9ecf817 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css @@ -10,7 +10,7 @@ border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border)); border-radius: 8px; box-shadow: 0 2px 8px var(--vscode-widget-shadow); - z-index: 10000; + z-index: 100; } .inline-chat-gutter-menu .input { diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 99591004f82..28c824b62be 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -109,6 +109,7 @@ export const CTX_INLINE_CHAT_EDITING = new RawContextKey('inlineChatEdi export const CTX_INLINE_CHAT_RESPONSE_FOCUSED = new RawContextKey('inlineChatResponseFocused', false, localize('inlineChatResponseFocused', "Whether the interactive widget's response is focused")); export const CTX_INLINE_CHAT_EMPTY = new RawContextKey('inlineChatEmpty', false, localize('inlineChatEmpty', "Whether the interactive editor input is empty")); export const CTX_INLINE_CHAT_INPUT_HAS_TEXT = new RawContextKey('inlineChatInputHasText', false, localize('inlineChatInputHasText', "Whether the inline chat input widget has text")); +export const CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED = new RawContextKey('inlineChatInputWidgetFocused', false, localize('inlineChatInputWidgetFocused', "Whether the inline chat input widget editor is focused")); export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey('inlineChatInnerCursorFirst', false, localize('inlineChatInnerCursorFirst', "Whether the cursor of the iteractive editor input is on the first line")); export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey('inlineChatInnerCursorLast', false, localize('inlineChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line")); export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); @@ -117,6 +118,7 @@ export const CTX_INLINE_CHAT_CHANGE_HAS_DIFF = new RawContextKey('inlin export const CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF = new RawContextKey('inlineChatChangeShowsDiff', false, localize('inlineChatChangeShowsDiff', "Whether the current change showing a diff")); export const CTX_INLINE_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('inlineChatRequestInProgress', false, localize('inlineChatRequestInProgress', "Whether an inline chat request is currently in progress")); export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey('inlineChatResponseType', InlineChatResponseType.None, localize('inlineChatResponseTypes', "What type was the responses have been receieved, nothing yet, just messages, or messaged and local edits")); +export const CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT = new RawContextKey('inlineChatFileBelongsToChat', false, localize('inlineChatFileBelongsToChat', "Whether the current file belongs to a chat editing session")); export const CTX_INLINE_CHAT_V1_ENABLED = ContextKeyExpr.or( ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, CTX_INLINE_CHAT_HAS_NOTEBOOK_INLINE) @@ -132,6 +134,7 @@ export const CTX_HOVER_MODE = ContextKeyExpr.equals('config.inlineChat.renderMod // --- (selected) action identifier export const ACTION_START = 'inlineChat.start'; +export const ACTION_ASK_IN_CHAT = 'inlineChat.askInChat'; export const ACTION_ACCEPT_CHANGES = 'inlineChat.acceptChanges'; export const ACTION_DISCARD_CHANGES = 'inlineChat.discardHunkChange'; export const ACTION_REGENERATE_RESPONSE = 'inlineChat.regenerate';