From a8bdc828e61b2fd5297a36d5b8a9a4d1bb4784f1 Mon Sep 17 00:00:00 2001 From: wsp Date: Thu, 27 Aug 2026 23:54:09 +0800 Subject: [PATCH 1/3] fix(flowchat): unify agent ID presentation Remove raw agent IDs from Task cards and present caller-selected IDs as readable labels in Agent control cards and the session tree. Reuse the formatted label for subagent panel and tab titles while preserving the raw ID for session routing and identity. --- .../modern/ModernFlowChatContainer.tsx | 2 +- .../modern/SessionTreePopover.test.tsx | 5 ++-- .../components/modern/SessionTreePopover.tsx | 24 ++++++++++++--- .../subagent-identity/agentIdDisplay.test.ts | 10 +++++++ .../subagent-identity/agentIdDisplay.ts | 7 +++++ .../src/flow_chat/subagent-identity/index.ts | 1 + .../tool-cards/AgentControlToolCard.test.tsx | 12 ++++++-- .../tool-cards/AgentControlToolCard.tsx | 8 +++-- .../flow_chat/tool-cards/TaskToolDisplay.scss | 10 ------- .../tool-cards/TaskToolDisplay.test.tsx | 10 +++---- .../flow_chat/tool-cards/TaskToolDisplay.tsx | 30 +------------------ 11 files changed, 62 insertions(+), 57 deletions(-) create mode 100644 src/web-ui/src/flow_chat/subagent-identity/agentIdDisplay.test.ts create mode 100644 src/web-ui/src/flow_chat/subagent-identity/agentIdDisplay.ts diff --git a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx index af8acce4f8..8065fc17f0 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx @@ -2361,7 +2361,7 @@ export const ModernFlowChatContainer: React.FC = ( parentSessionId: selection.parentSessionId, workspacePath: selection.workspacePath || activeSession.workspacePath, sessionKind: 'subagent', - sessionTitle: selection.agentId || selection.title, + sessionTitle: selection.displayTitle, agentType: selection.agentType, parentToolCallId: selection.parentToolCallId, subagentType: selection.subagentType, diff --git a/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.test.tsx b/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.test.tsx index be0c55c71e..2af5c4ba7c 100644 --- a/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.test.tsx +++ b/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.test.tsx @@ -142,7 +142,7 @@ describe('SessionTreePopover', () => { })); }); - it('maps sibling avatars from session IDs and uses Runtime agent IDs as names', async () => { + it('maps sibling avatars from session IDs and formats Runtime agent IDs as names', async () => { mocks.sessions.set('child-2', createSession('child-2', 'subagent', 'root')); mocks.sessions.set('child-3', createSession('child-3', 'subagent', 'root')); mocks.getSessionLineage.mockResolvedValue({ @@ -186,7 +186,7 @@ describe('SessionTreePopover', () => { expect(subagentNodes).toHaveLength(3); expect(subagentNodes.map(node => node.querySelector('.session-tree-popover__node-title')?.textContent)) - .toEqual(['parser-review', 'test-runner', 'docs-audit']); + .toEqual(['Parser review', 'Test runner', 'Docs audit']); }); it('closes a sibling action-menu portal and restores focus with the parent', async () => { @@ -273,6 +273,7 @@ describe('SessionTreePopover', () => { expect(onSelectSession).toHaveBeenCalledWith(expect.objectContaining({ sessionId: 'child', agentId: 'parser-review', + displayTitle: 'Parser review', isRoot: false, })); expect(onRequestClose).toHaveBeenCalledTimes(1); diff --git a/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.tsx b/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.tsx index d3808305a0..bf655b4df0 100644 --- a/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.tsx +++ b/src/web-ui/src/flow_chat/components/modern/SessionTreePopover.tsx @@ -24,6 +24,7 @@ import { type SessionLineageNode, } from '../../utils/sessionLineage'; import { + formatAgentIdForDisplay, SubagentAvatar, } from '../../subagent-identity'; import './SessionTreePopover.scss'; @@ -34,6 +35,7 @@ export interface SessionTreeSelection { parentSessionId?: string; parentToolCallId?: string; title: string; + displayTitle: string; agentType?: string; subagentType?: string; agentId?: string; @@ -69,6 +71,12 @@ function nodeHasActiveWork(node: SessionLineageNode): boolean { return node.lifecycle === 'running' || node.lifecycle === 'finishing'; } +function nodeDisplayTitle(node: SessionLineageNode): string { + return !node.isRoot && node.agentId + ? formatAgentIdForDisplay(node.agentId) + : node.title; +} + export const SessionTreePopover: React.FC = ({ sessionId, fallbackWorkspacePath, @@ -322,6 +330,7 @@ export const SessionTreePopover: React.FC = ({ parentSessionId: node.parentSessionId, parentToolCallId: node.parentToolCallId, title: node.title, + displayTitle: nodeDisplayTitle(node), agentType: node.agentType, subagentType: node.subagentType, agentId: node.agentId, @@ -362,6 +371,7 @@ export const SessionTreePopover: React.FC = ({ parentSessionId: node.parentSessionId, parentToolCallId: node.parentToolCallId, title: node.title, + displayTitle: nodeDisplayTitle(node), agentType: node.agentType, subagentType: node.subagentType, agentId: node.agentId, @@ -393,12 +403,16 @@ export const SessionTreePopover: React.FC = ({ ? t('flowChatHeader.agentTreeCancelling') : lifecycleLabel(node.lifecycle, t); const secondaryLabel = node.subagentType || node.agentType; - const primaryLabel = node.isRoot ? node.title : node.agentId || node.title; + const rawPrimaryLabel = node.isRoot ? node.title : node.agentId || node.title; + const primaryLabel = nodeDisplayTitle(node); + const descriptiveTitle = node.title !== primaryLabel && node.title !== rawPrimaryLabel + ? node.title + : undefined; const nodeMeta = node.isRoot ? secondaryLabel - : [secondaryLabel, node.title] + : [secondaryLabel, descriptiveTitle] .filter((value, index, values): value is string => - Boolean(value) && values.indexOf(value) === index && value !== primaryLabel + Boolean(value) && values.indexOf(value) === index ) .join(' · '); const canCancel = !!onCancelSession && !node.isRoot && nodeHasActiveWork(node); @@ -443,7 +457,9 @@ export const SessionTreePopover: React.FC = ({ onClick={() => handleSelect(node)} aria-label={node.isRoot ? undefined - : [primaryLabel, secondaryLabel, node.title, statusLabel].filter(Boolean).join(', ')} + : [primaryLabel, secondaryLabel, descriptiveTitle, statusLabel] + .filter(Boolean) + .join(', ')} > {node.isRoot ?