diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 743d980a58f39..3bcce432be5f1 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -18,7 +18,7 @@ import { IsLinuxContext, IsWindowsContext } from '../../../../../platform/contex import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js'; import { clearChatEditor } from './chatClear.js'; -import { CHAT_VIEW_ID, IChatWidgetService, showChatView } from '../chat.js'; +import { CHAT_VIEW_ID, IChatWidget, IChatWidgetService, showChatView } from '../chat.js'; import { IChatEditorOptions } from '../chatEditor.js'; import { ChatEditorInput } from '../chatEditorInput.js'; import { ChatViewPane } from '../chatViewPane.js'; @@ -36,6 +36,9 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { DropdownWithPrimaryActionViewItem } from '../../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js'; import { toAction } from '../../../../../base/common/actions.js'; +import { extractAgentAndCommand } from '../../common/chatParserTypes.js'; +import { Position } from '../../../../../editor/common/core/position.js'; +import { SuggestController } from '../../../../../editor/contrib/suggest/browser/suggestController.js'; export interface IChatViewTitleActionContext { chatView: ChatViewPane; @@ -245,10 +248,55 @@ class OpenChatEditorAction extends Action2 { } } + +class ChatAddAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.addParticipant', + title: localize2('chatWith', "Chat With Extension"), + icon: Codicon.plus, + f1: false, + category: CHAT_CATEGORY, + menu: { + id: MenuId.ChatInput, + group: 'navigation', + order: 1 + } + }); + } + + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const widgetService = accessor.get(IChatWidgetService); + const context: { widget?: IChatWidget } | undefined = args[0]; + const widget = context?.widget ?? widgetService.lastFocusedWidget; + if (!widget) { + return; + } + + const hasAgentOrCommand = extractAgentAndCommand(widget.parsedInput); + if (hasAgentOrCommand?.agentPart || hasAgentOrCommand?.commandPart) { + return; + } + + const suggestCtrl = SuggestController.get(widget.inputEditor); + if (suggestCtrl) { + const curText = widget.inputEditor.getValue(); + const newValue = curText ? `@ ${curText}` : '@'; + if (!curText.startsWith('@')) { + widget.inputEditor.setValue(newValue); + } + + widget.inputEditor.setPosition(new Position(1, 2)); + suggestCtrl.triggerSuggest(undefined, true); + } + } +} + export function registerChatActions() { registerAction2(OpenChatGlobalAction); registerAction2(ChatHistoryAction); registerAction2(OpenChatEditorAction); + registerAction2(ChatAddAction); registerAction2(class ClearChatInputHistoryAction extends Action2 { constructor() { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 577de41d44177..804a6dedccdc4 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -197,6 +197,7 @@ class AttachContextAction extends Action2 { when: AttachContextAction._cdt, id: MenuId.ChatInput, group: 'navigation', + order: 2 }, ] }); diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index a6dde83838a38..441d114bb8371 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -70,6 +70,7 @@ import { EditorOptions } from '../../../../editor/common/config/editorOptions.js import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { createInstantHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IChatViewState } from './chatWidget.js'; +import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js'; const $ = dom.$; @@ -523,6 +524,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const editorOptions = getSimpleCodeEditorWidgetOptions(); editorOptions.contributions?.push(...EditorExtensionsRegistry.getSomeEditorContributions([ContentHoverController.ID, GlyphHoverController.ID, CopyPasteController.ID])); this._inputEditor = this._register(scopedInstantiationService.createInstance(CodeEditorWidget, this._inputEditorElement, options, editorOptions)); + SuggestController.get(this._inputEditor)?.forceRenderingAbove(); this._register(this._inputEditor.onDidChangeModelContent(() => { const currentHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index ac59b5bf32789..4057ee86c6d08 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -503,7 +503,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const messageResult = this._register(renderer.render(welcomeContent.message)); dom.append(message, messageResult.element); - const tipsString = new MarkdownString(localize('chatWidget.tips', "{0} to attach context\n\n@ to chat with extensions", '$(attach)'), { supportThemeIcons: true }); + const tipsString = new MarkdownString(localize('chatWidget.tips', "{0} to attach context\n\n{1} to chat with extensions", '$(attach)', '$(plus)'), { supportThemeIcons: true }); const tipsResult = this._register(renderer.render(tipsString)); tips.appendChild(tipsResult.element); } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 0dfbb99c8aef5..d7c119925258a 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -4,39 +4,40 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { isPatternInWord } from '../../../../../base/common/filters.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { ResourceSet } from '../../../../../base/common/map.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; import { Position } from '../../../../../editor/common/core/position.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IWordAtPosition, getWordAtText } from '../../../../../editor/common/core/wordHelper.js'; -import { CompletionContext, CompletionItem, CompletionItemKind, CompletionList } from '../../../../../editor/common/languages.js'; +import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList } from '../../../../../editor/common/languages.js'; import { ITextModel } from '../../../../../editor/common/model.js'; import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; +import { SuggestController } from '../../../../../editor/contrib/suggest/browser/suggestController.js'; import { localize } from '../../../../../nls.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../common/contributions.js'; -import { SubmitAction } from '../actions/chatExecuteActions.js'; -import { IChatWidget, IChatWidgetService } from '../chat.js'; -import { ChatInputPart } from '../chatInputPart.js'; -import { ChatDynamicVariableModel, SelectAndInsertFileAction } from './chatDynamicVariables.js'; -import { ChatAgentLocation, getFullyQualifiedId, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../../common/chatAgents.js'; +import { IHistoryService } from '../../../../services/history/common/history.js'; +import { LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; +import { QueryBuilder } from '../../../../services/search/common/queryBuilder.js'; +import { ISearchService } from '../../../../services/search/common/search.js'; +import { ChatAgentLocation, IChatAgentData, IChatAgentNameService, IChatAgentService, getFullyQualifiedId } from '../../common/chatAgents.js'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestVariablePart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../common/chatParserTypes.js'; import { IChatSlashCommandService } from '../../common/chatSlashCommands.js'; import { IChatVariablesService, IDynamicVariable } from '../../common/chatVariables.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; -import { LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; -import { IHistoryService } from '../../../../services/history/common/history.js'; -import { ILabelService } from '../../../../../platform/label/common/label.js'; -import { ResourceSet } from '../../../../../base/common/map.js'; -import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; -import { isPatternInWord } from '../../../../../base/common/filters.js'; -import { ISearchService } from '../../../../services/search/common/search.js'; -import { QueryBuilder } from '../../../../services/search/common/queryBuilder.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { generateUuid } from '../../../../../base/common/uuid.js'; +import { SubmitAction } from '../actions/chatExecuteActions.js'; +import { IChatWidget, IChatWidgetService } from '../chat.js'; +import { ChatInputPart } from '../chatInputPart.js'; +import { ChatDynamicVariableModel, SelectAndInsertFileAction } from './chatDynamicVariables.js'; class SlashCommandCompletions extends Disposable { constructor( @@ -138,7 +139,7 @@ class AgentCompletions extends Disposable { insertText: `${agentLabel} `, detail: agent.description, range: new Range(1, 1, 1, 1), - command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent: agent, widget } satisfies AssignSelectedAgentActionArgs] }, + command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent: agent, widget, invokeProvider: subCommandProvider } satisfies AssignSelectedAgentActionArgs] }, kind: CompletionItemKind.Text, // The icons are disabled here anyway }; }) @@ -146,7 +147,7 @@ class AgentCompletions extends Disposable { } })); - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { + const subCommandProvider: CompletionItemProvider = { _debugDisplayName: 'chatAgentSubcommand', triggerCharacters: ['/'], provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { @@ -194,7 +195,8 @@ class AgentCompletions extends Disposable { }) }; } - })); + }; + this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, subCommandProvider)); // list subcommands when the query is empty, insert agent+subcommand this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { @@ -288,6 +290,7 @@ Registry.as(WorkbenchExtensions.Workbench).regi interface AssignSelectedAgentActionArgs { agent: IChatAgentData; widget: IChatWidget; + invokeProvider?: CompletionItemProvider; } class AssignSelectedAgentAction extends Action2 { @@ -307,6 +310,15 @@ class AssignSelectedAgentAction extends Action2 { } arg.widget.lastSelectedAgent = arg.agent; + + if (arg.invokeProvider) { + const suggestCtrl = SuggestController.get(arg.widget.inputEditor); + if (!suggestCtrl) { + return; + } + + suggestCtrl.triggerSuggest(new Set([arg.invokeProvider]), true); + } } } registerAction2(AssignSelectedAgentAction); @@ -486,6 +498,14 @@ export function computeCompletionRanges(model: ITextModel, position: Position, r // inside a "normal" word return; } + + if (!varWord && position.column > 1) { + const textBefore = model.getValueInRange(new Range(position.lineNumber, position.column - 1, position.lineNumber, position.column)); + if (textBefore !== ' ') { + return; + } + } + if (varWord && onlyOnWordStart) { const wordBefore = model.getWordUntilPosition({ lineNumber: position.lineNumber, column: varWord.startColumn }); if (wordBefore.word) { @@ -508,7 +528,7 @@ export function computeCompletionRanges(model: ITextModel, position: Position, r class VariableCompletions extends Disposable { - private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag + private static readonly VariableNameDef = new RegExp(`(?<=^|\\s)${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag constructor( @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index c44b4b8d63f28..5558b1784b3d8 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -584,7 +584,7 @@ export class StartVoiceChatAction extends Action2 { AnyScopedVoiceChatInProgress?.negate(), // hide when voice chat is in progress ), group: 'navigation', - order: -1 + order: 3 }] }); } @@ -624,7 +624,7 @@ export class StopListeningAction extends Action2 { id: MenuId.ChatInput, when: AnyScopedVoiceChatInProgress, group: 'navigation', - order: -1 + order: 3 }] }); } @@ -951,7 +951,7 @@ export class StopReadAloud extends Action2 { id: MenuId.ChatInput, when: ScopedChatSynthesisInProgress, group: 'navigation', - order: -1 + order: 3 }, ] }); @@ -1289,7 +1289,7 @@ export class InstallSpeechProviderForVoiceChatAction extends BaseInstallSpeechPr id: MenuId.ChatInput, when: HasSpeechProvider.negate(), group: 'navigation', - order: -1 + order: 3 }] }); }