From dbdc289aa90fc1167fd02a4039e2f1bbe518fbb2 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 3 Sep 2026 22:08:03 -0700 Subject: [PATCH] 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> --- .../test/browser/openSubagentChat.test.ts | 6 +- .../chat/browser/chat.shared.contribution.ts | 5 ++ .../chatContentParts/chatSubagentOpenChat.ts | 25 +++++- .../contrib/chat/common/constants.ts | 1 + .../chatSubagentContentPart.test.ts | 82 ++++++++++++++++++- 5 files changed, 113 insertions(+), 6 deletions(-) diff --git a/src/vs/sessions/contrib/providers/agentHost/test/browser/openSubagentChat.test.ts b/src/vs/sessions/contrib/providers/agentHost/test/browser/openSubagentChat.test.ts index 513bbb132307..7d2d3cf370d9 100644 --- a/src/vs/sessions/contrib/providers/agentHost/test/browser/openSubagentChat.test.ts +++ b/src/vs/sessions/contrib/providers/agentHost/test/browser/openSubagentChat.test.ts @@ -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('activeSession', undefined), visibleSessions: observableValue('visibleSessions', []), diff --git a/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts index 2afcd194db8b..85ef33db1f84 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts @@ -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."), diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentOpenChat.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentOpenChat.ts index 75a12af6f948..a47b2c138b6e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentOpenChat.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatSubagentOpenChat.ts @@ -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(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) { diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 7cb48c8f42ee..99371d79af9e 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -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', diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts index 5a11d77076ab..fa887531a5b2 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts @@ -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(); @@ -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(