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)

Reply via email to