diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index a4771342df3..79284a38e89 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -195,3 +195,10 @@ opacity: 0.7; margin-left: 0.5em; } + +.action-widget-delegate-label { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts index a90f752bf9b..e1dde593708 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../../base/common/codicons.js'; -import { IDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, IDisposable, markAsSingleton } from '../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../base/common/network.js'; import { basename } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -12,6 +13,7 @@ import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions import { isITextModel } from '../../../../../editor/common/model.js'; import { localize, localize2 } from '../../../../../nls.js'; import { ActionWidgetDropdownActionViewItem } from '../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js'; +import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; import { Action2, MenuId, MenuItemAction } from '../../../../../platform/actions/common/actions.js'; import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js'; import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider } from '../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; @@ -20,17 +22,28 @@ import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/cont import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; +import { ResourceContextKey } from '../../../../common/contextkeys.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; +import { chatEditingWidgetFileStateContextKey, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { ChatModel } from '../../common/chatModel.js'; import { ChatRequestParser } from '../../common/chatRequestParser.js'; import { IChatService } from '../../common/chatService.js'; import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; import { ChatAgentLocation } from '../../common/constants.js'; +import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/promptTypes.js'; import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../agentSessions/agentSessions.js'; import { IChatWidgetService } from '../chat.js'; import { CHAT_SETUP_ACTION_ID } from './chatActions.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { IChatRequestVariableEntry } from '../../common/chatVariableEntries.js'; + +export const enum ActionLocation { + ChatWidget = 'chatWidget', + Editor = 'editor' +} export class ContinueChatInSessionAction extends Action2 { @@ -46,12 +59,22 @@ export class ContinueChatInSessionAction extends Action2 { ChatContextKeys.requestInProgress.negate(), ChatContextKeys.remoteJobCreating.negate(), ), - menu: { + menu: [{ id: MenuId.ChatExecute, group: 'navigation', order: 3.4, when: ChatContextKeys.lockedToCodingAgent.negate(), + }, + { + id: MenuId.EditorContent, + group: 'continueIn', + when: ContextKeyExpr.and( + ContextKeyExpr.equals(ResourceContextKey.Scheme.key, Schemas.untitled), + ContextKeyExpr.equals(ResourceContextKey.LangId.key, PROMPT_LANGUAGE_ID), + ContextKeyExpr.notEquals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified), + ), } + ] }); } @@ -59,10 +82,10 @@ export class ContinueChatInSessionAction extends Action2 { // Handled by a custom action item } } - export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionViewItem { constructor( action: MenuItemAction, + private readonly location: ActionLocation, @IActionWidgetService actionWidgetService: IActionWidgetService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IKeybindingService keybindingService: IKeybindingService, @@ -71,12 +94,12 @@ export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionV @IOpenerService openerService: IOpenerService ) { super(action, { - actionProvider: ChatContinueInSessionActionItem.actionProvider(chatSessionsService, instantiationService), + actionProvider: ChatContinueInSessionActionItem.actionProvider(chatSessionsService, instantiationService, location), actionBarActions: ChatContinueInSessionActionItem.getActionBarActions(openerService) }, actionWidgetService, keybindingService, contextKeyService); } - private static getActionBarActions(openerService: IOpenerService) { + protected static getActionBarActions(openerService: IOpenerService) { const learnMoreUrl = 'https://aka.ms/vscode-continue-chat-in'; return [{ id: 'workbench.action.chat.continueChatInSession.learnMore', @@ -90,7 +113,7 @@ export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionV }]; } - private static actionProvider(chatSessionsService: IChatSessionsService, instantiationService: IInstantiationService): IActionWidgetDropdownActionProvider { + private static actionProvider(chatSessionsService: IChatSessionsService, instantiationService: IInstantiationService, location: ActionLocation): IActionWidgetDropdownActionProvider { return { getActions: () => { const actions: IActionWidgetDropdownAction[] = []; @@ -99,13 +122,13 @@ export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionV // Continue in Background const backgroundContrib = contributions.find(contrib => contrib.type === AgentSessionProviders.Background); if (backgroundContrib && backgroundContrib.canDelegate !== false) { - actions.push(this.toAction(AgentSessionProviders.Background, backgroundContrib, instantiationService)); + actions.push(this.toAction(AgentSessionProviders.Background, backgroundContrib, instantiationService, location)); } // Continue in Cloud const cloudContrib = contributions.find(contrib => contrib.type === AgentSessionProviders.Cloud); if (cloudContrib && cloudContrib.canDelegate !== false) { - actions.push(this.toAction(AgentSessionProviders.Cloud, cloudContrib, instantiationService)); + actions.push(this.toAction(AgentSessionProviders.Cloud, cloudContrib, instantiationService, location)); } // Offer actions to enter setup if we have no contributions @@ -119,7 +142,7 @@ export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionV }; } - private static toAction(provider: AgentSessionProviders, contrib: IChatSessionsExtensionPoint, instantiationService: IInstantiationService): IActionWidgetDropdownAction { + private static toAction(provider: AgentSessionProviders, contrib: IChatSessionsExtensionPoint, instantiationService: IInstantiationService, location: ActionLocation): IActionWidgetDropdownAction { return { id: contrib.type, enabled: true, @@ -128,7 +151,12 @@ export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionV description: `@${contrib.name}`, label: localize('continueSessionIn', "Continue in {0}", getAgentSessionProviderName(provider)), tooltip: contrib.displayName, - run: () => instantiationService.invokeFunction(accessor => new CreateRemoteAgentJobAction().run(accessor, contrib)) + run: () => instantiationService.invokeFunction(accessor => { + if (location === ActionLocation.Editor) { + return new CreateRemoteAgentJobFromEditorAction().run(accessor, contrib); + } + return new CreateRemoteAgentJobAction().run(accessor, contrib); + }) }; } @@ -148,10 +176,25 @@ export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionV } protected override renderLabel(element: HTMLElement): IDisposable | null { - const icon = this.contextKeyService.contextMatchesRules(ChatContextKeys.remoteJobCreating) ? Codicon.sync : Codicon.forward; - element.classList.add(...ThemeIcon.asClassNameArray(icon)); + if (this.location === ActionLocation.Editor) { + const container = document.createElement('span'); + container.classList.add('action-widget-delegate-label'); - return super.renderLabel(element); + const iconSpan = document.createElement('span'); + iconSpan.classList.add(...ThemeIcon.asClassNameArray(Codicon.forward)); + container.appendChild(iconSpan); + + const textSpan = document.createElement('span'); + textSpan.textContent = localize('delegate', "Delegate to..."); + container.appendChild(textSpan); + + element.appendChild(container); + return null; + } else { + const icon = this.contextKeyService.contextMatchesRules(ChatContextKeys.remoteJobCreating) ? Codicon.sync : Codicon.forward; + element.classList.add(...ThemeIcon.asClassNameArray(icon)); + return super.renderLabel(element); + } } } @@ -249,3 +292,67 @@ class CreateRemoteAgentJobAction { } } } + +class CreateRemoteAgentJobFromEditorAction { + constructor() { } + + async run(accessor: ServicesAccessor, continuationTarget: IChatSessionsExtensionPoint) { + + try { + const chatService = accessor.get(IChatService); + const continuationTargetType = continuationTarget.type; + const editorService = accessor.get(IEditorService); + const activeEditor = editorService.activeTextEditorControl; + const editorService2 = accessor.get(IEditorService); + + if (!activeEditor) { + return; + } + const model = activeEditor.getModel(); + if (!model || !isITextModel(model)) { + return; + } + const fileUri = model.uri as URI; + const chatModelReference = chatService.startSession(ChatAgentLocation.Chat, CancellationToken.None, {}); + const { sessionResource } = chatModelReference.object; + if (!sessionResource) { + return; + } + await editorService2.openEditor({ resource: sessionResource }, undefined); + const attachedContext: IChatRequestVariableEntry[] = [{ + kind: 'file', + id: 'vscode.implicit.selection', + name: basename(fileUri), + value: { + uri: fileUri + }, + }]; + await chatService.sendRequest(sessionResource, `Implement this.`, { + agentIdSilent: continuationTargetType, + attachedContext + }); + } catch (e) { + console.error('Error creating remote agent job from editor', e); + throw e; + } + } +} + +export class ContinueChatInSessionActionRendering extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'chat.continueChatInSessionActionRendering'; + + constructor( + @IActionViewItemService actionViewItemService: IActionViewItemService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + const disposable = actionViewItemService.register(MenuId.EditorContent, ContinueChatInSessionAction.ID, (action, options, instantiationService2) => { + if (!(action instanceof MenuItemAction)) { + return undefined; + } + return instantiationService.createInstance(ChatContinueInSessionActionItem, action, ActionLocation.Editor); + }); + markAsSingleton(disposable); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index aa4493f46a1..79a363e828c 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -68,6 +68,7 @@ import { ACTION_ID_NEW_CHAT, CopilotTitleBarMenuRendering, ModeOpenChatGlobalAct import { CodeBlockActionRendering, registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from './actions/chatCodeblockActions.js'; import { ChatContextContributions } from './actions/chatContext.js'; import { registerChatContextActions } from './actions/chatContextActions.js'; +import { ContinueChatInSessionActionRendering } from './actions/chatContinueInAction.js'; import { registerChatCopyActions } from './actions/chatCopyActions.js'; import { registerChatDeveloperActions } from './actions/chatDeveloperActions.js'; import { ChatSubmitAction, registerChatExecuteActions } from './actions/chatExecuteActions.js'; @@ -1123,6 +1124,7 @@ registerWorkbenchContribution2(ChatPromptFilesExtensionPointHandler.ID, ChatProm registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually); registerWorkbenchContribution2(CopilotTitleBarMenuRendering.ID, CopilotTitleBarMenuRendering, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(CodeBlockActionRendering.ID, CodeBlockActionRendering, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(ContinueChatInSessionActionRendering.ID, ContinueChatInSessionActionRendering, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatImplicitContextContribution.ID, ChatImplicitContextContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatRelatedFilesContribution.ID, ChatRelatedFilesContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatViewsWelcomeHandler.ID, ChatViewsWelcomeHandler, WorkbenchPhase.BlockStartup); diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 787e4659fa0..91fe3928a67 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -85,7 +85,7 @@ import { ChatHistoryNavigator } from '../common/chatWidgetHistoryService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind, validateChatMode } from '../common/constants.js'; import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js'; import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; -import { ChatContinueInSessionActionItem, ContinueChatInSessionAction } from './actions/chatContinueInAction.js'; +import { ActionLocation, ChatContinueInSessionActionItem, ContinueChatInSessionAction } from './actions/chatContinueInAction.js'; import { ChatOpenModelPickerActionId, ChatSessionPrimaryPickerAction, ChatSubmitAction, IChatExecuteActionContext, OpenModePickerAction } from './actions/chatExecuteActions.js'; import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; import { IChatWidget } from './chat.js'; @@ -1538,7 +1538,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge hiddenItemStrategy: HiddenItemStrategy.NoHide, actionViewItemProvider: (action, options) => { if (action.id === ContinueChatInSessionAction.ID && action instanceof MenuItemAction) { - return this.instantiationService.createInstance(ChatContinueInSessionActionItem, action); + return this.instantiationService.createInstance(ChatContinueInSessionActionItem, action, ActionLocation.ChatWidget); } return undefined; }