This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new ef069890b fix(ai): render tool results with unavailable names (#6438)
ef069890b is described below
commit ef069890b9f734a3ca16ea344436d9e8a2fdc1f7
Author: Loyal-Young <[email protected]>
AuthorDate: Thu Oct 15 18:10:30 2026 +0800
fix(ai): render tool results with unavailable names (#6438)
---
web/src/i18n/translations.ts | 1 +
.../ai/components/__tests__/ToolBlock.test.tsx | 43 ++++++++++++++++++++++
web/src/pages/ai/components/blocks/ToolBlock.tsx | 5 ++-
web/src/pages/ai/render/blocks.ts | 2 +
4 files changed, 49 insertions(+), 2 deletions(-)
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index b4d8b1ada..42a99c268 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -1863,6 +1863,7 @@ const translations: Record<string, Record<Lang, string>>
= {
en: 'What message types does RocketMQ have, and what scenarios is each one
for?',
},
'ai.tool.input': { zh: '输入参数', en: 'Input' },
+ 'ai.tool.unknownName': { zh: '未知工具', en: 'Unknown tool' },
'ai.tool.output': { zh: '执行结果', en: 'Result' },
'ai.tool.running': { zh: '执行中', en: 'Running' },
'ai.tool.success': { zh: '成功', en: 'Success' },
diff --git a/web/src/pages/ai/components/__tests__/ToolBlock.test.tsx
b/web/src/pages/ai/components/__tests__/ToolBlock.test.tsx
index 3e0ed7a51..90069f908 100644
--- a/web/src/pages/ai/components/__tests__/ToolBlock.test.tsx
+++ b/web/src/pages/ai/components/__tests__/ToolBlock.test.tsx
@@ -20,6 +20,8 @@ import { render, screen } from '@testing-library/react';
import { LangProvider } from '../../../../i18n/LangContext';
import type { TimelineEvent } from '../../../../api/aiEvents';
import { foldTimelineBlocks } from '../../render/foldTimeline';
+import { reduceLiveBlocks } from '../../render/reduceLive';
+import type { ChatSseEvent } from '../../../../api/aiEvents';
import { runningToolBlock } from '../../render/blocks';
import type { ToolBlock as ToolBlockData } from '../../render/blocks';
import ToolBlock from '../blocks/ToolBlock';
@@ -71,6 +73,47 @@ function foldOne(event: TimelineEvent): ToolBlockData {
}
describe('ToolBlock', () => {
+ it.each([
+ ['persisted', null],
+ ['persisted', undefined],
+ ['live', null],
+ ['live', undefined],
+ ] as const)('rendersAResultWithAnUnavailableToolNameTest: %s %s', (source,
tool) => {
+ // The backend projector permits a missing name when no tool start was
observed.
+ // JSON serialization also omits undefined names, as NON_NULL
serialization can do.
+ const wire = JSON.parse(
+ JSON.stringify({
+ ...ORPHANED_RESULT,
+ type: source === 'live' ? 'tool_done' : 'tool_result',
+ tool,
+ }),
+ );
+ const block = (
+ source === 'live'
+ ? reduceLiveBlocks([], wire as ChatSseEvent)[0]
+ : foldOne(wire as TimelineEvent)
+ ) as ToolBlockData;
+ render(
+ <LangProvider>
+ <ToolBlock
+ block={block}
+ toolCatalog={[
+ {
+ name: 'rmq.topic.list',
+ description: 'List topics',
+ parameters: {},
+ riskLevel: 'LOW',
+ },
+ ]}
+ />
+ </LangProvider>,
+ );
+ expect(screen.getByTestId('ai-tool-name')).toHaveTextContent('未知工具');
+ expect(screen.getByTestId('tool-result')).toHaveTextContent('TopicA');
+ expect(screen.getByTestId('ai-tool-status')).toHaveTextContent('成功');
+ expect(screen.queryByTestId('ai-tool-risk')).not.toBeInTheDocument();
+ });
+
it('rendersADoneCardWithNullInputForAnOrphanedToolResultTest', () => {
const block = foldOne(ORPHANED_RESULT);
diff --git a/web/src/pages/ai/components/blocks/ToolBlock.tsx
b/web/src/pages/ai/components/blocks/ToolBlock.tsx
index d1dd58119..ece2ccd7b 100644
--- a/web/src/pages/ai/components/blocks/ToolBlock.tsx
+++ b/web/src/pages/ai/components/blocks/ToolBlock.tsx
@@ -97,7 +97,8 @@ function resolveCatalogTool(
catalog: readonly McpTool[] | undefined,
tool: string,
): McpTool | undefined {
- if (!catalog || catalog.length === 0) return undefined;
+ if (typeof tool !== 'string' || !tool.trim() || !catalog || catalog.length
=== 0)
+ return undefined;
const exact = catalog.find((entry) => entry.name === tool);
if (exact) return exact;
const display = toolDisplayName(tool);
@@ -112,7 +113,7 @@ const ToolBlock = ({ block, toolCatalog }: ToolBlockProps)
=> {
const catalogTool = resolveCatalogTool(toolCatalog, block.tool);
const running = block.status === 'running';
const failed = block.status === 'done' && block.success === false;
- const displayName = toolDisplayName(block.tool);
+ const displayName = toolDisplayName(block.tool) || t('ai.tool.unknownName');
const inputText =
block.input === null || block.input === undefined
diff --git a/web/src/pages/ai/render/blocks.ts
b/web/src/pages/ai/render/blocks.ts
index 1b3dd8a15..990f73dda 100644
--- a/web/src/pages/ai/render/blocks.ts
+++ b/web/src/pages/ai/render/blocks.ts
@@ -121,6 +121,8 @@ const RMQ_NAMESPACE_PREFIX = 'rmq.';
* `Bash` -> `Bash`.
*/
export function toolDisplayName(tool: string): string {
+ // Orphaned results can carry a null/omitted name even though named calls
are the usual wire shape.
+ if (typeof tool !== 'string') return '';
const withoutMcp = tool.startsWith(MCP_TOOL_PREFIX) ?
tool.slice(MCP_TOOL_PREFIX.length) : tool;
return withoutMcp.startsWith(RMQ_NAMESPACE_PREFIX)
? withoutMcp.slice(RMQ_NAMESPACE_PREFIX.length)