Path: blob/main/src/vs/sessions/contrib/agentHost/browser/agentHostSkillButtons.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CancellationToken } from '../../../../base/common/cancellation.js';6import { Codicon } from '../../../../base/common/codicons.js';7import { Disposable } from '../../../../base/common/lifecycle.js';8import { ThemeIcon } from '../../../../base/common/themables.js';9import { localize2 } from '../../../../nls.js';10import { ILocalizedString } from '../../../../platform/action/common/action.js';11import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';12import { ContextKeyExpr, ContextKeyExpression, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';13import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';14import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js';15import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js';16import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';17import { ChatSendResult, IChatService } from '../../../../workbench/contrib/chat/common/chatService/chatService.js';18import { ChatAgentLocation } from '../../../../workbench/contrib/chat/common/constants.js';19import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';20import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';21import { ActiveSessionContextKeys, IsolationMode } from '../../changes/common/changes.js';22import { BaseAgentHostSessionsProvider } from './baseAgentHostSessionsProvider.js';2324/**25* True when the active session (in the Sessions window) is provided by an26* agent-host sessions provider (local or remote). Used to gate the built-in27* skill toolbar buttons and to suppress the Copilot CLI extension's own28* buttons for the same sessions.29*/30export const IsAgentHostSession = new RawContextKey<boolean>('sessions.isAgentHostSession', false);3132/**33* Binds {@link IsAgentHostSession} to the global context key service based on34* the active session's provider — true iff the provider is a35* {@link BaseAgentHostSessionsProvider}.36*/37export class IsAgentHostSessionContextContribution extends Disposable implements IWorkbenchContribution {3839static readonly ID = 'sessions.contrib.agentHost.isAgentHostSession';4041constructor(42@IContextKeyService contextKeyService: IContextKeyService,43@ISessionsManagementService sessionsManagementService: ISessionsManagementService,44@ISessionsProvidersService sessionsProvidersService: ISessionsProvidersService,45) {46super();4748this._register(bindContextKey(IsAgentHostSession, contextKeyService, reader => {49const activeSession = sessionsManagementService.activeSession.read(reader);50if (!activeSession) {51return false;52}53const provider = sessionsProvidersService.getProvider(activeSession.providerId);54return provider instanceof BaseAgentHostSessionsProvider;55}));56}57}5859registerWorkbenchContribution2(IsAgentHostSessionContextContribution.ID, IsAgentHostSessionContextContribution, WorkbenchPhase.AfterRestored);6061/**62* Toolbar buttons in the changes view that drive the built-in agent-host skills63* (`merge` / `create-pr` / `create-draft-pr` / `update-pr`) for any64* `agent-host-*` session.65*66* They mirror the buttons the Copilot CLI extension contributes for its own67* `chatSessionType == copilotcli` sessions, sending the same `/<skill-name>`68* prompt as if the user typed it. The skills themselves are bundled into every69* agent-host session via the synced customization bundler picking up70* {@link PromptsType.skill} entries with `BUILTIN_STORAGE`.71*/7273interface IAgentHostSkillButtonSpec {74readonly id: string;75readonly title: ILocalizedString;76readonly skill: string;77readonly icon: ThemeIcon;78readonly group: string;79readonly order: number;80readonly extraWhen: ContextKeyExpression | undefined;81}8283const AGENT_HOST_SKILL_BUTTON_ID_PREFIX = 'workbench.action.agentSessions.runSkill.';8485const AGENT_HOST_SKILL_BUTTONS: readonly IAgentHostSkillButtonSpec[] = [86{87id: `${AGENT_HOST_SKILL_BUTTON_ID_PREFIX}merge`,88title: localize2('agentSessions.runSkill.merge', "Merge Changes"),89skill: 'merge',90icon: Codicon.gitMerge,91group: 'merge',92order: 1,93extraWhen: ContextKeyExpr.and(94ActiveSessionContextKeys.IsolationMode.isEqualTo(IsolationMode.Worktree),95ActiveSessionContextKeys.IsMergeBaseBranchProtected.negate(),96ActiveSessionContextKeys.HasPullRequest.negate(),97ContextKeyExpr.or(ActiveSessionContextKeys.HasUncommittedChanges, ActiveSessionContextKeys.HasOutgoingChanges),98),99},100{101id: `${AGENT_HOST_SKILL_BUTTON_ID_PREFIX}createPR`,102title: localize2('agentSessions.runSkill.createPR', "Create Pull Request"),103skill: 'create-pr',104icon: Codicon.gitPullRequestCreate,105group: 'pull_request',106order: 1,107extraWhen: ContextKeyExpr.and(108ActiveSessionContextKeys.IsolationMode.isEqualTo(IsolationMode.Worktree),109ActiveSessionContextKeys.HasGitHubRemote,110ActiveSessionContextKeys.HasPullRequest.negate(),111ContextKeyExpr.or(ActiveSessionContextKeys.HasUncommittedChanges, ActiveSessionContextKeys.HasOutgoingChanges),112),113},114{115id: `${AGENT_HOST_SKILL_BUTTON_ID_PREFIX}createDraftPR`,116title: localize2('agentSessions.runSkill.createDraftPR', "Create Draft Pull Request"),117skill: 'create-draft-pr',118icon: Codicon.gitPullRequestDraft,119group: 'pull_request',120order: 2,121extraWhen: ContextKeyExpr.and(122ActiveSessionContextKeys.IsolationMode.isEqualTo(IsolationMode.Worktree),123ActiveSessionContextKeys.HasGitHubRemote,124ActiveSessionContextKeys.HasPullRequest.negate(),125ContextKeyExpr.or(ActiveSessionContextKeys.HasUncommittedChanges, ActiveSessionContextKeys.HasOutgoingChanges),126),127},128{129id: `${AGENT_HOST_SKILL_BUTTON_ID_PREFIX}updatePR`,130title: localize2('agentSessions.runSkill.updatePR', "Sync Pull Request"),131skill: 'update-pr',132icon: Codicon.repoPush,133group: 'pull_request',134order: 1,135extraWhen: ContextKeyExpr.and(136ActiveSessionContextKeys.IsolationMode.isEqualTo(IsolationMode.Worktree),137ActiveSessionContextKeys.HasGitHubRemote,138ActiveSessionContextKeys.HasPullRequest,139ActiveSessionContextKeys.HasOpenPullRequest,140),141},142];143144/**145* The `update-pr` button gets the same outgoing-changes count badge styling146* as the Copilot CLI extension's Sync PR button. Exported so the changes147* view can pick it out of the toolbar without re-deriving the ID.148*/149export const AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID = `${AGENT_HOST_SKILL_BUTTON_ID_PREFIX}updatePR`;150151/**152* True for any {@link Action2#id} created by this module. Used by the changes153* view to apply primary-button styling.154*/155export function isAgentHostSkillButtonId(actionId: string): boolean {156return actionId.startsWith(AGENT_HOST_SKILL_BUTTON_ID_PREFIX);157}158159function registerAgentHostSkillButton(spec: IAgentHostSkillButtonSpec): void {160registerAction2(class extends Action2 {161constructor() {162super({163id: spec.id,164title: spec.title,165icon: spec.icon,166f1: false,167menu: {168id: MenuId.ChatEditingSessionApplySubmenu,169group: spec.group,170order: spec.order,171when: ContextKeyExpr.and(172IsSessionsWindowContext,173IsAgentHostSession,174ActiveSessionContextKeys.HasGitRepository,175spec.extraWhen,176),177},178});179}180181async run(accessor: ServicesAccessor): Promise<void> {182const sessionsManagementService = accessor.get(ISessionsManagementService);183const chatService = accessor.get(IChatService);184185const activeSession = sessionsManagementService.activeSession.get();186if (!activeSession) {187return;188}189190// `activeSession.resource.scheme` matches the chat session191// contribution `type` registered for the agent-host (e.g.192// `agent-host-copilotcli`), which is the agent id the chat193// service uses for routing. The `sessionType` field is the194// logical, user-facing id (e.g. `copilotcli`) that is shared195// between the Copilot CLI extension and the local/remote196// agent-host providers, so it is NOT a valid agent id here.197const agentId = activeSession.resource.scheme;198const prompt = `/${spec.skill}`;199const ref = await chatService.acquireOrLoadSession(activeSession.resource, ChatAgentLocation.Chat, CancellationToken.None, 'AgentHostSkillButton');200try {201let result = await chatService.sendRequest(activeSession.resource, prompt, { agentIdSilent: agentId });202if (ChatSendResult.isQueued(result)) {203result = await result.deferred;204}205if (ChatSendResult.isSent(result)) {206await result.data.responseCompletePromise;207}208} finally {209ref?.dispose();210}211}212});213}214215for (const spec of AGENT_HOST_SKILL_BUTTONS) {216registerAgentHostSkillButton(spec);217}218219220