From 968736a97d181f2676a77d9e56009f9e9ddf4ec6 Mon Sep 17 00:00:00 2001 From: wsp Date: Thu, 13 Aug 2026 22:56:33 +0800 Subject: [PATCH] refactor(flow-chat): group non-critical tool cards Expand explore-group classification to include selected non-critical dedicated tools and tools using the default card renderer. Keep command, task, session-message, and other conditionally important tools visible as whole rounds. Exclude MCP tools from default-card classification and add registry and projection tests for these boundaries. --- .../store/modernFlowChatStore.test.ts | 84 ++++++++++++++++++- .../src/flow_chat/tool-cards/index.test.ts | 27 +++++- src/web-ui/src/flow_chat/tool-cards/index.ts | 2 + .../flow_chat/tool-cards/toolCardMetadata.ts | 76 +++++++++++++++-- 4 files changed, 181 insertions(+), 8 deletions(-) diff --git a/src/web-ui/src/flow_chat/store/modernFlowChatStore.test.ts b/src/web-ui/src/flow_chat/store/modernFlowChatStore.test.ts index b981ba592..970553b46 100644 --- a/src/web-ui/src/flow_chat/store/modernFlowChatStore.test.ts +++ b/src/web-ui/src/flow_chat/store/modernFlowChatStore.test.ts @@ -11,10 +11,26 @@ vi.mock('./FlowChatStore', () => ({ })); vi.mock('../tool-cards/toolCardMetadata', () => ({ - isCollapsibleTool: (toolName: string) => ['Read', 'LS', 'Grep', 'Glob', 'WebSearch', 'Bash', 'Git'].includes(toolName), + isCollapsibleTool: (toolName: string) => [ + 'Read', + 'LS', + 'Grep', + 'Glob', + 'WebSearch', + 'WebFetch', + 'GetFileDiff', + 'GetToolSpec', + 'ReviewSessionSummary', + 'TerminalControl', + 'SessionControl', + 'ExecControl', + 'view_image', + 'ReadCanvas', + 'ControlHub', + ].includes(toolName), READ_TOOL_NAMES: new Set(['Read']), SEARCH_TOOL_NAMES: new Set(['Grep', 'Glob', 'WebSearch']), - COMMAND_TOOL_NAMES: new Set(['Bash', 'Git']), + COMMAND_TOOL_NAMES: new Set(), })); import { sessionToVirtualItems, type VirtualItem } from './modernFlowChatStore'; @@ -136,6 +152,70 @@ describe('sessionToVirtualItems explore grouping', () => { expect(items.map(item => item.type)).toEqual(['user-message', 'explore-group']); }); + it.each([ + 'WebFetch', + 'GetFileDiff', + 'GetToolSpec', + 'ReviewSessionSummary', + 'TerminalControl', + 'SessionControl', + 'ExecControl', + 'view_image', + 'ReadCanvas', + 'ControlHub', + ])('collects non-critical %s rounds into explore groups', (toolName) => { + const session = makeSession({ + sessionId: `non-critical-${toolName}`, + dialogTurns: [{ + id: 'turn-1', + sessionId: `non-critical-${toolName}`, + userMessage: { + id: 'user-1', + content: 'Help', + timestamp: 900, + }, + modelRounds: [makeRound({ + items: [makeTool(`tool-${toolName}`, toolName)], + })], + status: 'completed', + startTime: 900, + }], + }); + + expect(sessionToVirtualItems(session).map(item => item.type)).toEqual([ + 'user-message', + 'explore-group', + ]); + }); + + it.each(['Bash', 'Git', 'ExecCommand', 'TodoWrite', 'ContextCompression', 'Skill', 'SessionMessage'])( + 'keeps conditionally important %s rounds visible', + (toolName) => { + const session = makeSession({ + sessionId: `critical-${toolName}`, + dialogTurns: [{ + id: 'turn-1', + sessionId: `critical-${toolName}`, + userMessage: { + id: 'user-1', + content: 'Help', + timestamp: 900, + }, + modelRounds: [makeRound({ + items: [makeTool(`tool-${toolName}`, toolName)], + })], + status: 'completed', + startTime: 900, + }], + }); + + expect(sessionToVirtualItems(session).map(item => item.type)).toEqual([ + 'user-message', + 'model-round', + ]); + }, + ); + it('projects the absolute Turn index for a sparse history-window message', () => { const session = makeSession({ sessionId: 'history-window-session', diff --git a/src/web-ui/src/flow_chat/tool-cards/index.test.ts b/src/web-ui/src/flow_chat/tool-cards/index.test.ts index 9ca9efaa3..64849fd9f 100644 --- a/src/web-ui/src/flow_chat/tool-cards/index.test.ts +++ b/src/web-ui/src/flow_chat/tool-cards/index.test.ts @@ -2,11 +2,36 @@ import { describe, expect, it } from 'vitest'; -import { getToolCardComponent } from './index'; +import { + DEDICATED_TOOL_CARD_NAMES, + getToolCardComponent, + isCollapsibleTool, + TOOL_CARD_COMPONENTS, + usesDefaultToolCard, +} from './index'; import { TaskToolDisplay } from './TaskToolDisplay'; describe('tool card registry', () => { it('projects managed Review workers through the unified coverage card', () => { expect(getToolCardComponent('LaunchReviewAgent')).toBe(TaskToolDisplay); }); + + it('keeps lightweight dedicated-card classification aligned with the component registry', () => { + expect([...DEDICATED_TOOL_CARD_NAMES].sort()).toEqual( + Object.keys(TOOL_CARD_COMPONENTS).sort(), + ); + }); + + it.each(['ControlHub', 'FinalizeMiniApp', 'PublishMiniApp', 'PublishAppearance'])( + 'treats %s as a default-card explore tool', + (toolName) => { + expect(usesDefaultToolCard(toolName)).toBe(true); + expect(isCollapsibleTool(toolName)).toBe(true); + }, + ); + + it('does not classify MCP tools as default-card explore tools', () => { + expect(usesDefaultToolCard('mcp__server__tool')).toBe(false); + expect(isCollapsibleTool('mcp__server__tool')).toBe(false); + }); }); diff --git a/src/web-ui/src/flow_chat/tool-cards/index.ts b/src/web-ui/src/flow_chat/tool-cards/index.ts index 8521acb99..0a0d2e8ca 100644 --- a/src/web-ui/src/flow_chat/tool-cards/index.ts +++ b/src/web-ui/src/flow_chat/tool-cards/index.ts @@ -14,9 +14,11 @@ export { READ_TOOL_NAMES, SEARCH_TOOL_NAMES, COMMAND_TOOL_NAMES, + DEDICATED_TOOL_CARD_NAMES, isCollapsibleTool, isCollapsibleItem, isCollapsibleItemWithContext, + usesDefaultToolCard, } from './toolCardMetadata'; const log = createLogger('ToolCardRegistry'); diff --git a/src/web-ui/src/flow_chat/tool-cards/toolCardMetadata.ts b/src/web-ui/src/flow_chat/tool-cards/toolCardMetadata.ts index a2419e584..07dc66da6 100644 --- a/src/web-ui/src/flow_chat/tool-cards/toolCardMetadata.ts +++ b/src/web-ui/src/flow_chat/tool-cards/toolCardMetadata.ts @@ -498,13 +498,79 @@ export function getAllToolNames(): string[] { // ==================== Collapsible explorer tools ==================== +/** + * Tools with a dedicated FlowChat card renderer. + * + * Keep this lightweight mirror aligned with TOOL_CARD_COMPONENTS. The registry + * test enforces equality so classification callers do not need to import every + * card component just to tell dedicated cards from the DefaultToolCard. + */ +export const DEDICATED_TOOL_CARD_NAMES = new Set([ + 'Read', + 'Write', + 'Edit', + 'Delete', + 'Grep', + 'Glob', + 'LS', + 'WebSearch', + 'WebFetch', + 'Task', + 'LaunchReviewAgent', + 'TodoWrite', + 'submit_code_review', + 'ContextCompression', + 'GetToolSpec', + 'Skill', + 'AskUserQuestion', + 'ReviewSessionSummary', + 'Git', + 'GetFileDiff', + 'CreatePlan', + 'TerminalControl', + 'SessionControl', + 'SessionMessage', + 'Bash', + 'ExecCommand', + 'WriteStdin', + 'ExecControl', + 'InitMiniApp', + 'PageDeploy', + 'PagePublish', + 'GenerativeUI', + 'ComputerUse', + 'view_image', + 'CreateCanvas', + 'ReadCanvas', + 'UpdateCanvas', + 'PatchCanvas', +]); + +/** Whether FlowChat renders this tool through DefaultToolCard. */ +export function usesDefaultToolCard(toolName: string): boolean { + return !isMcpToolName(toolName) && !DEDICATED_TOOL_CARD_NAMES.has(toolName); +} + /** - * Collapsible explorer tools. - * They are auto-collapsed during streaming to reduce visual noise. + * Explicit non-critical tools collected into explore groups. + * Tools rendered by DefaultToolCard are also collected; see isCollapsibleTool. */ export const COLLAPSIBLE_TOOL_NAMES = new Set([ - 'Read', 'LS', 'Grep', 'Glob', 'WebSearch', 'Bash', 'Git', + 'Read', + 'LS', + 'Grep', + 'Glob', + 'WebSearch', + 'WebFetch', + 'GetFileDiff', + 'GetToolSpec', + 'ReviewSessionSummary', + 'TerminalControl', + 'SessionControl', + 'ExecControl', + 'view_image', + 'ReadCanvas', ]); /** Read tools (counted in readCount). */ @@ -514,11 +580,11 @@ export const READ_TOOL_NAMES = new Set(['Read', 'LS']); export const SEARCH_TOOL_NAMES = new Set(['Grep', 'Glob', 'WebSearch']); /** Command tools (counted in commandCount). */ -export const COMMAND_TOOL_NAMES = new Set(['Bash', 'Git']); +export const COMMAND_TOOL_NAMES = new Set(); /** Check whether a tool is collapsible. */ export function isCollapsibleTool(toolName: string): boolean { - return COLLAPSIBLE_TOOL_NAMES.has(toolName); + return COLLAPSIBLE_TOOL_NAMES.has(toolName) || usesDefaultToolCard(toolName); } /**