chat: Add setting for subagent credit usage (#334384)

* chat: Add setting for subagent credit usage

Allow users to hide subagent credit usage from the pill, tooltip, and accessible label while preserving the existing default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chat: Default subagent credit usage to hidden

Keep credit usage opt-in and cover the tooltip alongside the visible and accessible presentation paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Justin Chen
2026-09-04 05:08:03 +00:00
committed by GitHub
co-authored by Copilot
parent 60e6485775
commit dbdc289aa9
5 changed files with 113 additions and 6 deletions
@@ -10,6 +10,9 @@ import { Event } from '../../../../../../base/common/event.js';
import { Disposable } from '../../../../../../base/common/lifecycle.js';
import { observableValue } from '../../../../../../base/common/observable.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js';
import { ChatConfiguration } from '../../../../../../workbench/contrib/chat/common/constants.js';
import { ILanguageModelsService } from '../../../../../../workbench/contrib/chat/common/languageModels.js';
import { workbenchInstantiationService } from '../../../../../../workbench/test/browser/workbenchTestServices.js';
import { ISessionsService } from '../../../../../services/sessions/browser/sessionsService.js';
@@ -235,8 +238,9 @@ suite('OpenSubagentChatActionViewItem', () => {
});
});
test('renders the credit cost alongside the model', () => {
test('renders the credit cost alongside the model when enabled', () => {
const instantiationService = workbenchInstantiationService(undefined, store);
(instantiationService.get(IConfigurationService) as TestConfigurationService).setUserConfiguration(ChatConfiguration.SubagentsShowCreditUsage, true);
instantiationService.stub(ISessionsService, {
activeSession: observableValue<IActiveSession | undefined>('activeSession', undefined),
visibleSessions: observableValue<readonly (IActiveSession | undefined)[]>('visibleSessions', []),
@@ -2450,6 +2450,11 @@ configurationRegistry.registerConfiguration({
description: nls.localize('chat.subagents.useRichRendering', "Controls whether subagents in chat editors use a rich presentation that opens each subagent in its own editor instead of rendering its full activity inline in the parent chat."),
default: true,
},
[ChatConfiguration.SubagentsShowCreditUsage]: {
type: 'boolean',
description: nls.localize('chat.subagents.showCreditUsage', "Controls whether AI credit usage is shown next to the duration for subagents."),
default: false,
},
[ChatConfiguration.TerminalAgentHostEnabled]: {
type: 'boolean',
description: nls.localize('chat.terminal.agentHost.enabled', "Controls whether Terminal Chat is backed by the Agent Host instead of the extension host. Applied on startup."),
@@ -21,6 +21,7 @@ import { IAccessibilityService } from '../../../../../../platform/accessibility/
import { IActionViewItemService } from '../../../../../../platform/actions/browser/actionViewItemService.js';
import { Action2, MenuId, MenuItemAction, registerAction2 } from '../../../../../../platform/actions/common/actions.js';
import { parseChatUri } from '../../../../../../platform/agentHost/common/state/sessionState.js';
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
import { IHoverService } from '../../../../../../platform/hover/browser/hover.js';
import { IInstantiationService, ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js';
import { IMarkdownRendererService } from '../../../../../../platform/markdown/browser/markdownRenderer.js';
@@ -30,7 +31,7 @@ import { ACTIVE_GROUP } from '../../../../../services/editor/common/editorServic
import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js';
import { formatElapsedTime } from '../../../common/chatProgressFormatting.js';
import { formatCopilotCreditsLabel } from '../../../common/chatService/chatService.js';
import { CHAT_OPEN_AGENT_HOST_CHAT_COMMAND_ID, CHAT_SUBAGENT_RESOURCE_QUERY_PARAM } from '../../../common/constants.js';
import { CHAT_OPEN_AGENT_HOST_CHAT_COMMAND_ID, CHAT_SUBAGENT_RESOURCE_QUERY_PARAM, ChatConfiguration } from '../../../common/constants.js';
import { AUTO_RAW_MODEL_ID, ILanguageModelsService } from '../../../common/languageModels.js';
import { IChatWidgetService } from '../../chat.js';
import { getChatMarkdownRenderOptions } from '../chatContentMarkdownRenderer.js';
@@ -256,6 +257,7 @@ export class OpenSubagentChatActionViewItem extends BaseActionViewItem {
@INotificationService notificationService: INotificationService,
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
@IHoverService private readonly hoverService: IHoverService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super(context, openInEditor ? createEditorOpenSubagentAction(action, chatWidgetService, notificationService) : createOpenSubagentAction(action), options);
this._sourceAction = action;
@@ -268,6 +270,12 @@ export class OpenSubagentChatActionViewItem extends BaseActionViewItem {
this._finishToolTransition();
}
}));
this._register(this.configurationService.onDidChangeConfiguration(event => {
if (event.affectsConfiguration(ChatConfiguration.SubagentsShowCreditUsage)) {
this._updateCredits();
this.updateTooltip();
}
}));
}
override render(container: HTMLElement): void {
@@ -423,14 +431,23 @@ export class OpenSubagentChatActionViewItem extends BaseActionViewItem {
private _setCredits(credits: number | undefined): void {
// Zero-cost subagents report 0 rather than nothing, so normalize both to hidden.
const show = typeof credits === 'number' && credits > 0;
this._reportedCredits = show ? credits : undefined;
this._reportedCredits = typeof credits === 'number' && credits > 0 ? credits : undefined;
this._updateCredits();
}
private _updateCredits(): void {
const credits = this._reportedCredits;
const show = credits !== undefined && this._showCreditUsage;
if (this._creditsElement) {
this._creditsElement.textContent = show ? formatCopilotCreditsLabel(credits) : '';
this._creditsElement.classList.toggle('hidden', !show);
}
}
private get _showCreditUsage(): boolean {
return this.configurationService.getValue<boolean>(ChatConfiguration.SubagentsShowCreditUsage) === true;
}
private _setAgentType(agentType: string | undefined): void {
this._reportedAgentType = agentType;
if (this._agentTypeElement) {
@@ -662,7 +679,7 @@ export class OpenSubagentChatActionViewItem extends BaseActionViewItem {
if (this._reportedModelName) {
details.push(localize('chat.subagent.modelTooltip', "Model: {0}", this._reportedModelName));
}
if (this._reportedCredits !== undefined) {
if (this._reportedCredits !== undefined && this._showCreditUsage) {
details.push(formatCopilotCreditsLabel(this._reportedCredits));
}
if (this._displayedToolAccessibleLabel && this._displayedActivityIsTool) {
@@ -94,6 +94,7 @@ export enum ChatConfiguration {
SubagentsAllowInvocationsFromSubagents = 'chat.subagents.allowInvocationsFromSubagents',
SubagentsDefaultToAuto = 'chat.subagents.defaultToAuto',
SubagentsUseRichRendering = 'chat.subagents.useRichRendering',
SubagentsShowCreditUsage = 'chat.subagents.showCreditUsage',
ShowCodeBlockProgressAnimation = 'chat.agent.codeBlockProgress',
RestoreLastPanelSession = 'chat.restoreLastPanelSession',
ExitAfterDelegation = 'chat.exitAfterDelegation',
@@ -30,7 +30,7 @@ import { IRenderedMarkdown, MarkdownRenderOptions } from '../../../../../../../b
import { IMarkdownString, isMarkdownString } from '../../../../../../../base/common/htmlContent.js';
import { EditorPool, DiffEditorPool } from '../../../../browser/widget/chatContentParts/chatContentCodePools.js';
import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js';
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js';
import { ConfigurationTarget, IConfigurationChangeEvent, IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js';
import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js';
import { AccessibilityWorkbenchSettingId } from '../../../../../accessibility/browser/accessibilityConfiguration.js';
import { URI } from '../../../../../../../base/common/uri.js';
@@ -55,6 +55,12 @@ class TestOpenChatActionViewItem extends ActionViewItem {
}
}
class TestOpenSubagentChatActionViewItem extends OpenSubagentChatActionViewItem {
get tooltip(): string | undefined {
return this.getTooltip();
}
}
class TestActionViewItemService implements IActionViewItemService {
declare _serviceBrand: undefined;
private readonly _onDidChange = new Emitter<MenuId>();
@@ -340,6 +346,7 @@ suite('ChatSubagentContentPart', () => {
));
instantiationService.stub(IMenuService, menuService);
(instantiationService.get(IConfigurationService) as TestConfigurationService).setUserConfiguration(ChatConfiguration.SubagentsUseRichRendering, true);
(instantiationService.get(IConfigurationService) as TestConfigurationService).setUserConfiguration(ChatConfiguration.SubagentsShowCreditUsage, true);
// Mock list pool and editor pool
mockListPool = {} as CollapsibleListPool;
@@ -524,6 +531,79 @@ suite('ChatSubagentContentPart', () => {
});
});
test('should update displayed and accessible credit usage when the setting changes', () => {
const configService = instantiationService.get(IConfigurationService) as TestConfigurationService;
const setShowCreditUsage = (value: boolean) => {
configService.setUserConfiguration(ChatConfiguration.SubagentsShowCreditUsage, value);
configService.onDidChangeConfigurationEmitter.fire({
source: ConfigurationTarget.USER,
affectedKeys: new Set([ChatConfiguration.SubagentsShowCreditUsage]),
change: { keys: [ChatConfiguration.SubagentsShowCreditUsage], overrides: [] },
affectsConfiguration: key => key === ChatConfiguration.SubagentsShowCreditUsage,
} satisfies IConfigurationChangeEvent);
};
const action = store.add(new Action('openSubagent', 'Open Subagent'));
const viewItem = store.add(instantiationService.createInstance(
TestOpenSubagentChatActionViewItem,
{
chatResource: 'ahp-chat://subagent/Y29waWxvdGNsaTovc2Vzc2lvbg/tool-call',
parentSessionResource: 'agent-host-copilotcli:/session',
startedAt: 1_000,
duration: 65_000,
credits: 2.5,
},
action,
{},
false,
));
const container = mainWindow.document.createElement('div');
viewItem.render(container);
const credits = container.querySelector('.chat-subagent-pill-credits');
const before = {
text: credits?.textContent,
hidden: credits?.classList.contains('hidden'),
tooltip: viewItem.tooltip,
ariaLabel: container.getAttribute('aria-label'),
};
setShowCreditUsage(false);
const hidden = {
text: credits?.textContent,
hidden: credits?.classList.contains('hidden'),
tooltip: viewItem.tooltip,
ariaLabel: container.getAttribute('aria-label'),
};
setShowCreditUsage(true);
const restored = {
text: credits?.textContent,
hidden: credits?.classList.contains('hidden'),
tooltip: viewItem.tooltip,
ariaLabel: container.getAttribute('aria-label'),
};
assert.deepStrictEqual({ before, hidden, restored }, {
before: {
text: '2.5 credits',
hidden: false,
tooltip: 'Open Subagent\n2.5 credits',
ariaLabel: 'Open Subagent. Worked for 1m 5s. 2.5 credits',
},
hidden: {
text: '',
hidden: true,
tooltip: 'Open Subagent',
ariaLabel: 'Open Subagent. Worked for 1m 5s',
},
restored: {
text: '2.5 credits',
hidden: false,
tooltip: 'Open Subagent\n2.5 credits',
ariaLabel: 'Open Subagent. Worked for 1m 5s. 2.5 credits',
},
});
});
test('should render the specialized subagent type before the title', () => {
const action = store.add(new Action('openSubagent', 'Open Subagent'));
const viewItem = store.add(instantiationService.createInstance(