Skip to content

Commit 9c6b9d1

Browse files
justschenCopilot
andauthored
chat: clarify background activity and unify subagent titles (#338503)
* chat: clarify background activity and unify subagent titles Report running subagents and background commands independently of parent progress, including work from earlier requests. Refresh retained progress on activity changes while preserving user-action prompts and completion behavior. Use descriptive subagent chat titles for coordination tools and notifications in live sessions and restored history. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chat: fix progress announcements and fixture expectations Keep subsequent stalled-tool notifications replaceable without overriding renderer-supplied progress. Announce idle background activity changes politely while respecting verbose progress settings. Update component fixture assertions to the running-count labels and cover repeated fallback notifications and accessible count updates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a91a0cf commit 9c6b9d1

16 files changed

Lines changed: 505 additions & 175 deletions

File tree

‎src/vs/platform/agentHost/common/agent.ts‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -731,14 +731,7 @@ export interface IAgentSpawnChatEvent {
731731
/** Max characters for a subagent tab title before it is ellipsized. */
732732
const SUBAGENT_CHAT_TITLE_MAX_LENGTH = 60;
733733

734-
/**
735-
* Builds the tab title for a subagent peer chat. Prefers the concise
736-
* per-task description (so two subagents of the same type still get
737-
* distinct, meaningful names), truncating it so an over-long value never
738-
* blows out the tab strip or the Subagents dropdown; falls back to the
739-
* agent type's display name, then a generic label. Shared by the live
740-
* spawn path and the restore path so both name subagent tabs identically.
741-
*/
734+
/** Builds the shared title for subagent chats and activity messages, preferring a concise task description over the agent type's name. */
742735
export function subagentChatTitle(taskDescription: string | undefined, agentDisplayName: string | undefined): string {
743736
const task = taskDescription?.trim();
744737
if (task) {

‎src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import { gitHubMcpServerUrl } from '../../common/githubEndpoints.js';
4242
import { getSessionSandboxConfig } from '../sessionSandbox.js';
4343
import { AgentHostAutoApprovePolicyRestrictedConfigKey, AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostAutoReplyAnswer, AgentHostAutoReplyEnabledConfigKey, AgentHostDisableRepoInfoTelemetryConfigKey, platformRootSchema, platformSessionSchema } from '../../common/agentHostSchema.js';
4444
import { createUnknownAgentHostClientTelemetryContext, type IAgentHostClientTelemetryContext } from '../../common/agentHostTelemetry.js';
45-
import { AgentSession, AgentSignal, AgentWorkingDirectoryChangedError, AuthenticateParams, IMcpNotification, type AgentSubagentTaskModelSource, type AgentTurnProviderCallState, type IAgentPendingMessageSender, type IAgentPermissionResponseContext, type IAgentTelemetryContext, type IAgentToolPendingConfirmationSignal, type IAgentTurnDiagnosticSnapshot, type IAgentTurnTokenUsage } from '../../common/agent.js';
45+
import { AgentSession, AgentSignal, AgentWorkingDirectoryChangedError, AuthenticateParams, IMcpNotification, subagentChatTitle, type AgentSubagentTaskModelSource, type AgentTurnProviderCallState, type IAgentPendingMessageSender, type IAgentPermissionResponseContext, type IAgentTelemetryContext, type IAgentToolPendingConfirmationSignal, type IAgentTurnDiagnosticSnapshot, type IAgentTurnTokenUsage } from '../../common/agent.js';
4646
import { isReasoningEffortLevel } from '../../common/reasoningEffort.js';
4747
import { ObservedTokenUsage } from './observedTokenUsage.js';
4848
import { META_DIFF_BASE_BRANCH } from '../../common/agentHostGitService.js';
@@ -1824,7 +1824,7 @@ export class CopilotAgentSession extends Disposable {
18241824
if (task.type !== 'agent') {
18251825
continue;
18261826
}
1827-
const displayName = task.displayName?.trim() || task.description.trim();
1827+
const displayName = subagentChatTitle(task.description, task.displayName);
18281828
if (displayName && !this._subagentDisplayNamesByAgentId.get(task.id)?.trim()) {
18291829
this._subagentDisplayNamesByAgentId.set(task.id, displayName);
18301830
}
@@ -5517,7 +5517,7 @@ export class CopilotAgentSession extends Disposable {
55175517

55185518
this._register(wrapper.onSystemNotification(e => {
55195519
this._seedSubagentDisplayNames([e]);
5520-
const notification = buildCopilotSystemNotification(e);
5520+
const notification = buildCopilotSystemNotification(e, this._resolveAgentName);
55215521
if (!notification) {
55225522
this._logService.trace(`[Copilot:${sessionId}] Ignoring system.notification kind=${e.data.kind.type}`);
55235523
return;
@@ -6384,17 +6384,17 @@ export class CopilotAgentSession extends Disposable {
63846384
this._logService.error(`[Copilot:${sessionId}] subagent.started emitted after cancellation; dropping`);
63856385
return;
63866386
}
6387+
const tracked = this._activeToolCalls.get(e.data.toolCallId);
63876388
if (e.agentId) {
63886389
this._parentToolCallIdsByAgentId.set(e.agentId, e.data.toolCallId);
6389-
this._subagentDisplayNamesByAgentId.set(e.agentId, e.data.agentDisplayName);
6390+
this._subagentDisplayNamesByAgentId.set(e.agentId, subagentChatTitle(tracked?.meta?.subagentDescription, e.data.agentDisplayName));
63906391
this._activeSubagentAgentIds.add(e.agentId);
63916392
this._subagentTaskCompletionSchedulers.deleteAndDispose(e.agentId);
63926393
}
63936394
if (this._currentTurn.value) {
63946395
this._rootTurnIdBySubagentToolCallId.set(e.data.toolCallId, this._currentTurn.value.id);
63956396
}
63966397
this._logService.info(`[Copilot:${sessionId}] Subagent started: toolCallId=${e.data.toolCallId}, agent=${e.data.agentName}`);
6397-
const tracked = this._activeToolCalls.get(e.data.toolCallId);
63986398
this._onDidSessionProgress.fire({
63996399
kind: 'subagent_started',
64006400
chat: this._chatChannelUri,

‎src/vs/platform/agentHost/node/copilot/copilotSystemNotification.ts‎

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type { SessionEvent, SessionEventPayload, SystemNotification } from '@git
77
import { softAssertNever } from '../../../../base/common/assert.js';
88
import { appendEscapedMarkdownInlineCode } from '../../../../base/common/htmlContent.js';
99
import { localize } from '../../../../nls.js';
10+
import { subagentChatTitle } from '../../common/agent.js';
11+
import { getSubagentMetadata, getToolKind, type ToolAgentNameResolver } from './copilotToolDisplay.js';
1012

1113
export interface ICopilotSystemNotification {
1214
/** Text for a new system-origin AHP turn; derived from SDK `data.kind` metadata, e.g. shell completion `description`. */
@@ -15,34 +17,59 @@ export interface ICopilotSystemNotification {
1517
readonly startsTurn: boolean;
1618
}
1719

18-
function getCopilotSubagentDisplayInfo(event: SessionEvent): { agentId: string; displayName: string } | undefined {
20+
function getCopilotSubagentDisplayInfo(event: SessionEvent, taskDescriptions?: ReadonlyMap<string, string>): { agentId: string; displayName: string } | undefined {
1921
if (event.type === 'subagent.started' || event.type === 'subagent.completed' || event.type === 'subagent.failed') {
22+
const description = taskDescriptions?.get(event.data.toolCallId);
2023
const displayName = event.data.agentDisplayName.trim();
21-
return event.agentId && displayName ? { agentId: event.agentId, displayName } : undefined;
24+
return event.agentId && (description || displayName)
25+
? { agentId: event.agentId, displayName: subagentChatTitle(description, displayName) }
26+
: undefined;
2227
}
2328
if (event.type === 'system.notification') {
2429
const kind = event.data.kind;
2530
if (kind.type === 'agent_completed' || kind.type === 'agent_idle') {
26-
const displayName = kind.displayName?.trim() || kind.description?.trim() || kind.agentType.trim();
27-
return displayName ? { agentId: kind.agentId, displayName } : undefined;
31+
const displayName = kind.displayName?.trim() || kind.agentType.trim();
32+
return kind.description?.trim() || displayName
33+
? { agentId: kind.agentId, displayName: subagentChatTitle(kind.description, displayName) }
34+
: undefined;
2835
}
2936
}
3037
return undefined;
3138
}
3239

33-
/** Collects agent labels without letting notification fallbacks replace canonical lifecycle names. */
40+
/** Reconstructs the chat titles used by agent activity, preferring spawning task descriptions over SDK names. */
3441
export function getCopilotSubagentDisplayNames(events: readonly SessionEvent[]): ReadonlyMap<string, string> {
42+
const taskDescriptions = new Map<string, string>();
43+
const collectTaskDescription = (toolCallId: string, toolName: string, parameters: unknown) => {
44+
if (getToolKind(toolName) === 'subagent') {
45+
const description = getSubagentMetadata(parameters).description?.trim();
46+
if (description) {
47+
taskDescriptions.set(toolCallId, description);
48+
}
49+
}
50+
};
51+
for (const event of events) {
52+
if (event.type === 'tool.execution_start') {
53+
collectTaskDescription(event.data.toolCallId, event.data.toolName, event.data.arguments);
54+
} else if (event.type === 'assistant.message') {
55+
for (const request of event.data.toolRequests ?? []) {
56+
if (!taskDescriptions.has(request.toolCallId)) {
57+
collectTaskDescription(request.toolCallId, request.name, request.arguments);
58+
}
59+
}
60+
}
61+
}
3562
const names = new Map<string, string>();
3663
for (const event of events) {
37-
const identity = getCopilotSubagentDisplayInfo(event);
64+
const identity = getCopilotSubagentDisplayInfo(event, taskDescriptions);
3865
if (identity && (event.type !== 'system.notification' || !names.has(identity.agentId))) {
3966
names.set(identity.agentId, identity.displayName);
4067
}
4168
}
4269
return names;
4370
}
4471

45-
export function buildCopilotSystemNotification(event: SessionEventPayload<'system.notification'>): ICopilotSystemNotification | undefined {
72+
export function buildCopilotSystemNotification(event: SessionEventPayload<'system.notification'>, resolveAgentName?: ToolAgentNameResolver): ICopilotSystemNotification | undefined {
4673
const data = event.data;
4774
const kind: SystemNotification = data.kind;
4875
const content = cleanSystemNotificationContent(data.content);
@@ -63,7 +90,7 @@ export function buildCopilotSystemNotification(event: SessionEventPayload<'syste
6390
}
6491
case 'agent_completed':
6592
case 'agent_idle': {
66-
const name = getCopilotSubagentDisplayInfo(event)?.displayName;
93+
const name = resolveAgentName?.(kind.agentId)?.trim() || getCopilotSubagentDisplayInfo(event)?.displayName;
6794
const formattedName = name ? appendEscapedMarkdownInlineCode(name) : undefined;
6895
if (kind.type === 'agent_idle') {
6996
return {

‎src/vs/platform/agentHost/node/copilot/copilotToolDisplay.ts‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,8 @@ interface ICopilotLongRunningSearchToolArgs {
231231
export type ToolAgentNameResolver = (agentId: string) => string | undefined;
232232

233233
/**
234-
* Resolves opaque SDK agent ids to their display names for presentation only; invocation
235-
* arguments are untouched. Unknown ids and blank names fall back to the raw id.
234+
* Resolves SDK agent ids to their subagent chat titles for presentation without changing invocation arguments.
235+
* Unknown ids and blank titles fall back to the raw id.
236236
*/
237237
function getAgentLabel(agentId: unknown, resolveAgentName: ToolAgentNameResolver | undefined): string | undefined {
238238
if (typeof agentId !== 'string' || agentId.length === 0) {
@@ -1055,15 +1055,16 @@ export function getToolKind(toolName: string, parameters?: Record<string, unknow
10551055
*
10561056
* Only call this for tools where {@link getToolKind} returned `'subagent'`.
10571057
*/
1058-
export function getSubagentMetadata(parameters: Record<string, unknown> | undefined): { agentName?: string; description?: string } {
1059-
if (!parameters) {
1058+
export function getSubagentMetadata(parameters: unknown): { agentName?: string; description?: string } {
1059+
if (!isObject(parameters)) {
10601060
return {};
10611061
}
1062-
const agentName = typeof parameters.agent_type === 'string' && parameters.agent_type.length > 0
1063-
? parameters.agent_type
1062+
const metadata = parameters as Record<string, unknown>;
1063+
const agentName = typeof metadata.agent_type === 'string' && metadata.agent_type.length > 0
1064+
? metadata.agent_type
10641065
: undefined;
1065-
const description = typeof parameters.description === 'string' && parameters.description.length > 0
1066-
? parameters.description
1066+
const description = typeof metadata.description === 'string' && metadata.description.length > 0
1067+
? metadata.description
10671068
: undefined;
10681069
return { agentName, description };
10691070
}

‎src/vs/platform/agentHost/node/copilot/mapSessionEvents.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@ export async function mapSessionEvents(
716716
break;
717717
}
718718
case 'system.notification': {
719-
const notification = buildCopilotSystemNotification(e);
719+
const notification = buildCopilotSystemNotification(e, resolveAgentName);
720720
if (!notification) {
721721
break;
722722
}

0 commit comments

Comments
 (0)