Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/vs/sessions/contrib/changes/browser/changesView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ import { EditorResourceAccessor, SideBySideEditor } from '../../../../workbench/
import { logChangesViewFileSelect, logChangesViewVersionModeChange, logChangesViewViewModeChange } from '../../../common/sessionsTelemetry.js';
import { ChecksViewModel } from './checksViewModel.js';
// eslint-disable-next-line local/code-import-patterns -- TODO: move skill button constants out of providers
import { AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID, isAgentHostSkillButtonId } from '../../providers/agentHost/browser/agentHostSkillButtons.js';
import { AGENT_HOST_SKILL_BUTTON_COMMIT_ID, AGENT_HOST_SKILL_BUTTON_SYNC_ID, AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID, isAgentHostSkillButtonId } from '../../providers/agentHost/browser/agentHostSkillButtons.js';
import { ActiveSessionContextKeys, CHANGES_VIEW_CONTAINER_ID, CHANGES_VIEW_ID, ChangesContextKeys, ChangesViewMode, IsolationMode } from '../common/changes.js';
import { buildTreeChildren, ChangesTreeElement, ChangesTreeRenderer, IChangesFileItem, IChangesTreeRootInfo, isChangesFileItem, toIChangesFileItem } from './changesViewRenderer.js';
import { ChangesViewModel } from './changesViewModel.js';
Expand Down Expand Up @@ -211,6 +211,25 @@ class ChangesButtonBarWidget extends Disposable {
}
return { showIcon: false, showLabel: true, isSecondary: false, customLabel: `$(loading) ${labelWithCount}` };
}
if (action.id === AGENT_HOST_SKILL_BUTTON_COMMIT_ID) {
if (!hasGitOperationInProgress) {
return { showIcon: true, showLabel: true, isSecondary: false };
}
const customLabelObs = derived(reader => {
const running = runningLabelObs.read(reader);
return `$(loading) ${running ?? action.label}`;
});
return { showIcon: false, showLabel: true, isSecondary: false, customLabelObs };
}
Comment on lines +214 to +223
if (action.id === AGENT_HOST_SKILL_BUTTON_SYNC_ID) {
const labelWithCount = outgoingChanges > 0
? `${action.label} ${outgoingChanges}↑`
: action.label;
if (!hasGitOperationInProgress) {
return { showIcon: true, showLabel: true, isSecondary: false, customLabel: labelWithCount };
}
return { showIcon: false, showLabel: true, isSecondary: false, customLabel: `$(loading) ${labelWithCount}` };
}
if (
action.id === 'github.copilot.claude.sessions.sync' ||
action.id === AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,43 @@ interface IAgentHostSkillButtonSpec {
readonly group: string;
readonly order: number;
readonly extraWhen: ContextKeyExpression | undefined;
/** Menu to register the button on. Defaults to {@link MenuId.AgentsChangesPrimaryActionSubMenu}. */
readonly menuId?: MenuId;
}

const AGENT_HOST_SKILL_BUTTON_ID_PREFIX = 'workbench.action.agentSessions.runSkill.';

const AGENT_HOST_SKILL_BUTTONS: readonly IAgentHostSkillButtonSpec[] = [
{
id: `${AGENT_HOST_SKILL_BUTTON_ID_PREFIX}commit`,
title: localize2('agentSessions.runSkill.commit', "Commit"),
skill: 'commit',
icon: Codicon.check,
group: 'navigation',
order: 0,
menuId: MenuId.AgentsChangesToolbar,
Comment on lines +89 to +96
extraWhen: ContextKeyExpr.and(
ActiveSessionContextKeys.IsolationMode.isEqualTo(IsolationMode.Worktree),
ActiveSessionContextKeys.HasUncommittedChanges,
),
},
{
id: `${AGENT_HOST_SKILL_BUTTON_ID_PREFIX}sync`,
title: localize2('agentSessions.runSkill.sync', "Sync"),
skill: 'sync',
icon: Codicon.sync,
group: 'navigation',
order: 0,
menuId: MenuId.AgentsChangesToolbar,
extraWhen: ContextKeyExpr.and(
ActiveSessionContextKeys.IsolationMode.isEqualTo(IsolationMode.Worktree),
ActiveSessionContextKeys.HasUpstream,
ContextKeyExpr.or(
ActiveSessionContextKeys.HasIncomingChanges,
ActiveSessionContextKeys.HasOutgoingChanges,
),
),
},
{
id: `${AGENT_HOST_SKILL_BUTTON_ID_PREFIX}merge`,
title: localize2('agentSessions.runSkill.merge', "Merge Changes"),
Expand Down Expand Up @@ -152,6 +184,8 @@ const AGENT_HOST_SKILL_BUTTONS: readonly IAgentHostSkillButtonSpec[] = [
* as the Copilot CLI extension's Sync PR button. Exported so the changes
* view can pick it out of the toolbar without re-deriving the ID.
*/
export const AGENT_HOST_SKILL_BUTTON_COMMIT_ID = `${AGENT_HOST_SKILL_BUTTON_ID_PREFIX}commit`;
export const AGENT_HOST_SKILL_BUTTON_SYNC_ID = `${AGENT_HOST_SKILL_BUTTON_ID_PREFIX}sync`;
export const AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID = `${AGENT_HOST_SKILL_BUTTON_ID_PREFIX}updatePR`;
Comment on lines 184 to 189

/**
Expand All @@ -171,7 +205,7 @@ function registerAgentHostSkillButton(spec: IAgentHostSkillButtonSpec): void {
icon: spec.icon,
f1: false,
menu: {
id: MenuId.AgentsChangesPrimaryActionSubMenu,
id: spec.menuId ?? MenuId.AgentsChangesPrimaryActionSubMenu,
group: spec.group,
order: spec.order,
when: ContextKeyExpr.and(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { IChat } from '../../../../../services/sessions/common/session.js';
import { ISessionsProvidersService } from '../../../../../services/sessions/browser/sessionsProvidersService.js';
import { ISessionsProvider } from '../../../../../services/sessions/common/sessionsProvider.js';
import { IActiveSession, ISessionsManagementService } from '../../../../../services/sessions/common/sessionsManagement.js';
import { AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID, IsAgentHostSession, IsAgentHostSessionContextContribution, isAgentHostSkillButtonId } from '../../browser/agentHostSkillButtons.js';
import { AGENT_HOST_SKILL_BUTTON_COMMIT_ID, AGENT_HOST_SKILL_BUTTON_SYNC_ID, AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID, IsAgentHostSession, IsAgentHostSessionContextContribution, isAgentHostSkillButtonId } from '../../browser/agentHostSkillButtons.js';
import { BaseAgentHostSessionsProvider } from '../../browser/baseAgentHostSessionsProvider.js';
// Importing this contribution registers the apply submenu on the changes toolbar,
// which is the slot that hosts our skill buttons as a dropdown.
Expand Down Expand Up @@ -163,8 +163,8 @@ suite('agentHostSkillButtons - menu registration', () => {

ensureNoDisposablesAreLeakedInTestSuite();

function skillButtonItems() {
const all = MenuRegistry.getMenuItems(MenuId.AgentsChangesPrimaryActionSubMenu);
function skillButtonItems(menuId: MenuId = MenuId.AgentsChangesPrimaryActionSubMenu) {
const all = MenuRegistry.getMenuItems(menuId);
const menuItems: { command: { id: string }; when?: ContextKeyExpression }[] = [];
for (const item of all) {
if (!isIMenuItem(item)) {
Expand All @@ -187,8 +187,16 @@ suite('agentHostSkillButtons - menu registration', () => {
]);
});

test('registers commit and sync skill buttons on the changes toolbar', () => {
const ids = skillButtonItems(MenuId.AgentsChangesToolbar).map(item => item.command.id).sort();
assert.deepStrictEqual(ids, [
'workbench.action.agentSessions.runSkill.commit',
'workbench.action.agentSessions.runSkill.sync',
]);
});

test('every skill button `when` clause includes sessions.isAgentHostSession and isSessionsWindow', () => {
for (const item of skillButtonItems()) {
for (const item of [...skillButtonItems(), ...skillButtonItems(MenuId.AgentsChangesToolbar)]) {
const whenStr = item.when?.serialize() ?? '';
assert.ok(
whenStr.includes(IsAgentHostSession.key),
Expand All @@ -207,6 +215,18 @@ suite('agentHostSkillButtons - menu registration', () => {
`expected command ${AGENT_HOST_SKILL_BUTTON_UPDATE_PR_ID} to be registered`);
});

test('exported commit id matches the registered command', () => {
assert.ok(isAgentHostSkillButtonId(AGENT_HOST_SKILL_BUTTON_COMMIT_ID));
assert.ok(CommandsRegistry.getCommand(AGENT_HOST_SKILL_BUTTON_COMMIT_ID),
`expected command ${AGENT_HOST_SKILL_BUTTON_COMMIT_ID} to be registered`);
});

test('exported sync id matches the registered command', () => {
assert.ok(isAgentHostSkillButtonId(AGENT_HOST_SKILL_BUTTON_SYNC_ID));
assert.ok(CommandsRegistry.getCommand(AGENT_HOST_SKILL_BUTTON_SYNC_ID),
`expected command ${AGENT_HOST_SKILL_BUTTON_SYNC_ID} to be registered`);
});

test('the apply submenu is contributed to the changes toolbar in the navigation group', () => {
const toolbarItems = MenuRegistry.getMenuItems(MenuId.AgentsChangesToolbar);
const submenuEntry = toolbarItems.find(item => isISubmenuItem(item) && item.submenu === MenuId.AgentsChangesPrimaryActionSubMenu);
Expand Down