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 85f89c959 fix(ai): stop showing unfinished tools as running after 
termination (#6419)
85f89c959 is described below

commit 85f89c95944f56e2b625ffee1b2489a67f727160
Author: Loyal-Young <[email protected]>
AuthorDate: Thu Oct 15 18:12:21 2026 +0800

    fix(ai): stop showing unfinished tools as running after termination (#6419)
---
 web/src/i18n/translations.ts                       |  5 +++
 web/src/pages/ai/components/AssistantBubble.tsx    |  1 +
 .../components/__tests__/AssistantBubble.test.tsx  | 39 ++++++++++++++++++++++
 web/src/pages/ai/components/blocks/ToolBlock.tsx   | 26 +++++++++++----
 4 files changed, 64 insertions(+), 7 deletions(-)

diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 42a99c268..f7d9cf1e3 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -1866,6 +1866,11 @@ const translations: Record<string, Record<Lang, string>> 
= {
   'ai.tool.unknownName': { zh: '未知工具', en: 'Unknown tool' },
   'ai.tool.output': { zh: '执行结果', en: 'Result' },
   'ai.tool.running': { zh: '执行中', en: 'Running' },
+  'ai.tool.missingResult': { zh: '未返回结果', en: 'No result' },
+  'ai.tool.outputMissing': {
+    zh: '本次运行已结束,未收到工具结果',
+    en: 'The run ended without a tool result',
+  },
   'ai.tool.success': { zh: '成功', en: 'Success' },
   'ai.tool.failed': { zh: '失败', en: 'Failed' },
   'ai.tool.truncated': { zh: '已截断({size})', en: 'Truncated ({size})' },
diff --git a/web/src/pages/ai/components/AssistantBubble.tsx 
b/web/src/pages/ai/components/AssistantBubble.tsx
index 9e149b7c2..c230f3f25 100644
--- a/web/src/pages/ai/components/AssistantBubble.tsx
+++ b/web/src/pages/ai/components/AssistantBubble.tsx
@@ -233,6 +233,7 @@ const AssistantBubble = ({
                     key={`tool-${index}-${block.tcId}`}
                     block={block}
                     toolCatalog={toolCatalog}
+                    runStatus={bubble.runStatus}
                   />
                 );
               case 'notice':
diff --git a/web/src/pages/ai/components/__tests__/AssistantBubble.test.tsx 
b/web/src/pages/ai/components/__tests__/AssistantBubble.test.tsx
index 98c716059..a8d39192c 100644
--- a/web/src/pages/ai/components/__tests__/AssistantBubble.test.tsx
+++ b/web/src/pages/ai/components/__tests__/AssistantBubble.test.tsx
@@ -70,6 +70,45 @@ const TOOL_RESULT = {
   success: true,
 };
 
+describe('terminal tool rendering', () => {
+  it.each(['STOPPED', 'FAILED', 'COMPLETED'] as const)(
+    'doesNotClaimAnUnfinishedToolIsStillRunningAfter%sTest',
+    (runStatus) => {
+      const pendingTool = runningToolBlock('pending', 'rmq.topic.list', { 
instanceId: 'prod' });
+      const { rerender } = render(
+        <LangProvider>
+          <AssistantBubble bubble={bubble([pendingTool])} streaming />
+        </LangProvider>,
+      );
+      expect(screen.getByTestId('ai-tool-progress')).toBeInTheDocument();
+      rerender(
+        <LangProvider>
+          <AssistantBubble bubble={bubble([pendingTool], { runStatus })} />
+        </LangProvider>,
+      );
+      expect(screen.queryByTestId('ai-tool-progress')).not.toBeInTheDocument();
+      const card = screen.getByTestId('ai-tool-block');
+      expect(within(card).queryByText('执行中')).not.toBeInTheDocument();
+      expect(within(card).queryByText('成功')).not.toBeInTheDocument();
+      expect(within(card).queryByText('失败')).not.toBeInTheDocument();
+      expect(within(card).getByText('未返回结果')).toBeInTheDocument();
+      expect(within(card).getByTestId('tool-input')).toHaveTextContent('prod');
+      expect(pendingTool.status).toBe('running');
+    },
+  );
+
+  it('keepsAnObservedResultOnATerminalRunTest', () => {
+    renderBubble({
+      blocks: withToolResult([], TOOL_RESULT),
+      bubbleExtra: { runStatus: 'STOPPED' },
+    });
+    const card = screen.getByTestId('ai-tool-block');
+    expect(within(card).getByText('成功')).toBeInTheDocument();
+    
expect(within(card).getByTestId('tool-result')).toHaveTextContent('TopicA');
+    expect(within(card).queryByText('未返回结果')).not.toBeInTheDocument();
+  });
+});
+
 function bubble(blocks: RenderBlock[], extra: Partial<Bubble> = {}): Bubble {
   return { role: 'assistant', blocks, ...extra };
 }
diff --git a/web/src/pages/ai/components/blocks/ToolBlock.tsx 
b/web/src/pages/ai/components/blocks/ToolBlock.tsx
index ece2ccd7b..9b0eefc4d 100644
--- a/web/src/pages/ai/components/blocks/ToolBlock.tsx
+++ b/web/src/pages/ai/components/blocks/ToolBlock.tsx
@@ -17,9 +17,10 @@
 
 import { memo, type CSSProperties } from 'react';
 import { Progress, Tag, theme } from 'antd';
-import { CaretRight, CheckCircle, CircleNotch, XCircle } from 
'@phosphor-icons/react';
+import { CaretRight, CheckCircle, CircleNotch, MinusCircle, XCircle } from 
'@phosphor-icons/react';
 import { useLang } from '../../../../i18n/LangContext';
 import type { McpTool } from '../../../../api/ai';
+import type { RunStatus } from '../../../../api/aiEvents';
 import type { ToolBlock as ToolBlockData } from '../../render/blocks';
 import { toolDisplayName } from '../../render/blocks';
 import { formatBytes } from '../../../../utils/format';
@@ -78,6 +79,7 @@ const usePreStyle = (): CSSProperties => {
 
 export interface ToolBlockProps {
   block: ToolBlockData;
+  runStatus?: RunStatus;
   /** Catalog from `listTools()`, used for the risk tag. Optional: no catalog, 
no tag. */
   toolCatalog?: readonly McpTool[];
 }
@@ -105,13 +107,15 @@ function resolveCatalogTool(
   return catalog.find((entry) => toolDisplayName(entry.name) === display);
 }
 
-const ToolBlock = ({ block, toolCatalog }: ToolBlockProps) => {
+const ToolBlock = ({ block, toolCatalog, runStatus }: ToolBlockProps) => {
   const { t } = useLang();
   const { token } = theme.useToken();
   const preStyle = usePreStyle();
 
   const catalogTool = resolveCatalogTool(toolCatalog, block.tool);
-  const running = block.status === 'running';
+  const ended = runStatus === 'COMPLETED' || runStatus === 'STOPPED' || 
runStatus === 'FAILED';
+  const missingResult = block.status === 'running' && ended;
+  const running = block.status === 'running' && !ended;
   const failed = block.status === 'done' && block.success === false;
   const displayName = toolDisplayName(block.tool) || t('ai.tool.unknownName');
 
@@ -133,7 +137,7 @@ const ToolBlock = ({ block, toolCatalog }: ToolBlockProps) 
=> {
   return (
     <details
       data-testid="ai-tool-block"
-      data-status={block.status}
+      data-status={missingResult ? 'missing-result' : block.status}
       className="ai-tool-card"
       open={running || undefined}
       style={{
@@ -161,6 +165,8 @@ const ToolBlock = ({ block, toolCatalog }: ToolBlockProps) 
=> {
       >
         {running ? (
           <CircleNotch size={16} className="ai-icon-spin" style={{ color: 
token.colorPrimary }} />
+        ) : missingResult ? (
+          <MinusCircle size={16} style={{ color: token.colorTextSecondary }} />
         ) : failed ? (
           <XCircle size={16} weight="fill" style={{ color: token.colorError }} 
/>
         ) : (
@@ -191,10 +197,16 @@ const ToolBlock = ({ block, toolCatalog }: 
ToolBlockProps) => {
         )}
         <Tag
           data-testid="ai-tool-status"
-          color={running ? 'processing' : failed ? 'error' : 'success'}
+          color={running ? 'processing' : missingResult ? 'default' : failed ? 
'error' : 'success'}
           style={{ marginInlineEnd: 0, fontSize: 14, flexShrink: 0 }}
         >
-          {running ? t('ai.tool.running') : failed ? t('ai.tool.failed') : 
t('ai.tool.success')}
+          {running
+            ? t('ai.tool.running')
+            : missingResult
+              ? t('ai.tool.missingResult')
+              : failed
+                ? t('ai.tool.failed')
+                : t('ai.tool.success')}
         </Tag>
         {block.durationMs !== null && (
           <span style={{ fontSize: 14, color: token.colorTextTertiary, 
flexShrink: 0 }}>
@@ -251,7 +263,7 @@ const ToolBlock = ({ block, toolCatalog }: ToolBlockProps) 
=> {
         <summary style={summaryStyle}>{t('ai.tool.output')}</summary>
         {block.output === null ? (
           <div style={{ marginTop: 6, fontSize: 14, color: 
token.colorTextTertiary }}>
-            {t('ai.tool.outputPending')}
+            {t(missingResult ? 'ai.tool.outputMissing' : 
'ai.tool.outputPending')}
           </div>
         ) : (
           <pre data-testid="tool-result" style={{ ...preStyle, marginTop: 6 }}>

Reply via email to