This is an automated email from the ASF dual-hosted git repository.

voidmatcha pushed a commit to branch 
branch-voidmatcha-assistant-integration-temp
in repository https://gitbox.apache.org/repos/asf/zeppelin.git

commit 9fd079b214112ffe4dc13897da445ebac18049ef
Author: YONGJAE LEE <[email protected]>
AuthorDate: Fri Oct 16 12:42:19 2026 +0900

    [ZEPPELIN-6745] Align answer UI with segmented history
---
 .../entities/assistant/ui/AssistantAnswer.spec.tsx | 53 ++++++++++++++++++++--
 .../assistant/ui/AssistantAnswer.stories.tsx       | 19 +++++---
 .../src/entities/assistant/ui/AssistantAnswer.tsx  | 13 ++++--
 .../assistant/ui/AssistantMessageItem.stories.tsx  |  6 ++-
 .../src/entities/assistant/ui/AssistantToolLog.tsx |  9 +++-
 .../src/entities/assistant/ui/answerView.ts        |  6 +--
 6 files changed, 85 insertions(+), 21 deletions(-)

diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.spec.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.spec.tsx
index 7531cf429e..e1a3ed4253 100644
--- 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.spec.tsx
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.spec.tsx
@@ -42,8 +42,10 @@ describe('AssistantAnswer', () => {
           message: {
             id: 'm1',
             role: 'assistant',
-            content: 'Revenue is grouped by region.',
-            toolCalls: [{ id: 't1', name: 'list_paragraphs' }]
+            segments: [
+              { id: 's1', content: '', toolCalls: [{ id: 't1', name: 
'list_paragraphs' }] },
+              { id: 's2', content: 'Revenue is grouped by region.' }
+            ]
           }
         }}
       />
@@ -97,8 +99,7 @@ describe('AssistantAnswer', () => {
           message: {
             id: 'm1',
             role: 'assistant',
-            content: 'Revenue',
-            toolCalls: [{ id: 't1', name: 'list_paragraphs' }]
+            segments: [{ id: 's1', content: 'Revenue', toolCalls: [{ id: 't1', 
name: 'list_paragraphs' }] }]
           }
         }}
       />
@@ -111,6 +112,28 @@ describe('AssistantAnswer', () => {
     expect(screen.getByRole('status').textContent).toBe('Running Tool…');
   });
 
+  it('keeps completed answer rounds in order and shows tools from every 
round', () => {
+    render(
+      <AssistantAnswer
+        view={{
+          status: 'done',
+          message: {
+            id: 'm2',
+            role: 'assistant',
+            segments: [
+              { id: 'm1', content: 'First finding', toolCalls: [{ id: 't1', 
name: 'list_paragraphs' }] },
+              { id: 'm2', content: 'Second finding', toolCalls: [{ id: 't2', 
name: 'read_results' }] }
+            ]
+          }
+        }}
+      />
+    );
+
+    expect(answer().textContent).toContain('First finding\n\nSecond finding');
+    expect(screen.getByText('Used 2 tools')).toBeTruthy();
+    expect(screen.getAllByRole('listitem').map(item => 
item.textContent)).toEqual(['list_paragraphs', 'read_results']);
+  });
+
   it('keeps the partial answer and replaces the progress line with the error', 
() => {
     const view: AssistantAnswerView = {
       status: 'error',
@@ -125,10 +148,30 @@ describe('AssistantAnswer', () => {
     expect(screen.getByRole('alert').textContent).toContain('stopped 
responding');
   });
 
+  it('does not imply a tool is still observed after the connection is lost', 
() => {
+    render(
+      <AssistantAnswer
+        view={{
+          status: 'error',
+          kind: 'connection',
+          message: 'Connection lost',
+          tools: [{ id: 't1', name: 'read_results', status: 'running' }]
+        }}
+      />
+    );
+
+    expect(screen.getByText('Used 1 tool (status unknown)')).toBeTruthy();
+    expect(screen.queryByRole('img', { name: 'Running' })).toBeNull();
+    expect(screen.getByRole('img', { name: 'Status unknown' })).toBeTruthy();
+  });
+
   it('renders answer bodies through the given renderer', () => {
     render(
       <AssistantAnswer
-        view={{ status: 'done', message: { id: 'm1', role: 'assistant', 
content: '**bold**' } }}
+        view={{
+          status: 'done',
+          message: { id: 'm1', role: 'assistant', segments: [{ id: 's1', 
content: '**bold**' }] }
+        }}
         renderContent={content => <strong>{content.replaceAll('*', 
'')}</strong>}
       />
     );
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.stories.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.stories.tsx
index cedeff5d6e..dc472e2696 100644
--- 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.stories.tsx
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.stories.tsx
@@ -30,16 +30,22 @@ const fixtures = {
       { id: 't2', name: 'list_paragraphs', status: 'running' }
     ]
   },
-  completed: { status: 'done', message: { id: 'a1', role: 'assistant', 
content: reply } },
+  completed: { status: 'done', message: { id: 'a1', role: 'assistant', 
segments: [{ id: 's1', content: reply }] } },
   completedWithTools: {
     status: 'done',
     message: {
       id: 'a1',
       role: 'assistant',
-      content: reply,
-      toolCalls: [
-        { id: 't1', name: 'list_paragraphs' },
-        { id: 't2', name: 'list_paragraphs' }
+      segments: [
+        {
+          id: 's1',
+          content: '',
+          toolCalls: [
+            { id: 't1', name: 'list_paragraphs' },
+            { id: 't2', name: 'list_paragraphs' }
+          ]
+        },
+        { id: 's2', content: reply }
       ]
     }
   },
@@ -55,7 +61,8 @@ const fixtures = {
     status: 'error',
     kind: 'connection',
     message: new AssistantConnectionError().message,
-    content: partial
+    content: partial,
+    tools: [{ id: 't1', name: 'read_results', status: 'running' }]
   }
 } satisfies Record<string, AssistantAnswerView>;
 
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.tsx
index b0f6fbecbc..1139c73b2e 100644
--- 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.tsx
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantAnswer.tsx
@@ -11,6 +11,7 @@
  */
 
 import { Skeleton } from 'antd';
+import { answerText, answerToolCalls } from '../model/messageHistory';
 import type { AssistantContentRenderer, AssistantAnswerView, 
AssistantToolActivity } from './answerView';
 import { AssistantAnswerError } from './AssistantAnswerError';
 import { AssistantMessageItem } from './AssistantMessageItem';
@@ -61,19 +62,23 @@ export const AssistantAnswer = ({ view, onRetry, 
renderContent }: AssistantAnswe
       );
     }
     case 'done': {
-      const tools = (view.message.toolCalls ?? []).map(call => ({ ...call, 
status: 'done' as const }));
+      const tools = answerToolCalls(view.message).map(call => ({ ...call, 
status: 'done' as const }));
       return (
-        <AssistantMessageItem role="assistant" content={view.message.content} 
renderContent={renderContent}>
+        <AssistantMessageItem role="assistant" 
content={answerText(view.message)} renderContent={renderContent}>
           <AssistantToolLog tools={tools} />
         </AssistantMessageItem>
       );
     }
-    case 'error':
+    case 'error': {
+      const tools = (view.tools ?? []).map(tool =>
+        tool.status === 'running' ? { ...tool, status: 'unknown' as const } : 
tool
+      );
       return (
         <AssistantMessageItem role="assistant" content={view.content ?? ''} 
renderContent={renderContent}>
-          <AssistantToolLog tools={view.tools ?? []} />
+          <AssistantToolLog tools={tools} />
           <AssistantAnswerError kind={view.kind} message={view.message} 
onRetry={onRetry} />
         </AssistantMessageItem>
       );
+    }
   }
 };
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.stories.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.stories.tsx
index 75931eb2b7..69ba1ccff4 100644
--- 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.stories.tsx
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantMessageItem.stories.tsx
@@ -54,7 +54,11 @@ export const Conversation: Story = {
       <AssistantAnswer
         view={{
           status: 'done',
-          message: { id: 'a1', role: 'assistant', content: reply, toolCalls: 
[{ id: 't1', name: 'list_paragraphs' }] }
+          message: {
+            id: 'a1',
+            role: 'assistant',
+            segments: [{ id: 's1', content: reply, toolCalls: [{ id: 't1', 
name: 'list_paragraphs' }] }]
+          }
         }}
       />
     </div>
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantToolLog.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantToolLog.tsx
index 46a0a111a6..aa1359733a 100644
--- 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantToolLog.tsx
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/AssistantToolLog.tsx
@@ -10,7 +10,7 @@
  * limitations under the License.
  */
 
-import { CheckOutlined } from '@ant-design/icons';
+import { CheckOutlined, QuestionCircleOutlined } from '@ant-design/icons';
 import { Spin } from 'antd';
 import type { AssistantToolActivity } from './answerView';
 import * as styles from './AssistantAnswer.css';
@@ -25,10 +25,13 @@ export interface AssistantToolLogProps {
 export const AssistantToolLog = ({ tools }: AssistantToolLogProps) => {
   if (!tools.length) return null;
   const running = tools.some(tool => tool.status === 'running');
+  const unknown = tools.some(tool => tool.status === 'unknown');
   const count = `${tools.length} ${tools.length === 1 ? 'tool' : 'tools'}`;
   return (
     <details className={styles.toolLog} open={running}>
-      <summary className={styles.toolSummary}>{running ? `Using ${count}` : 
`Used ${count}`}</summary>
+      <summary className={styles.toolSummary}>
+        {running ? `Using ${count}` : unknown ? `Used ${count} (status 
unknown)` : `Used ${count}`}
+      </summary>
       <ul className={styles.toolList}>
         {tools.map(tool => (
           <li key={tool.id} className={styles.toolItem} 
data-status={tool.status}>
@@ -36,6 +39,8 @@ export const AssistantToolLog = ({ tools }: 
AssistantToolLogProps) => {
               <span className={styles.toolIcon} role="img" 
aria-label="Running">
                 <Spin size="small" />
               </span>
+            ) : tool.status === 'unknown' ? (
+              <QuestionCircleOutlined className={styles.toolIcon} 
aria-label="Status unknown" />
             ) : (
               <CheckOutlined className={`${styles.toolIcon} 
${styles.toolDone}`} aria-label="Done" />
             )}
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/answerView.ts
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/answerView.ts
index b11e7ebcfc..f154658c79 100644
--- 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/answerView.ts
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/ui/answerView.ts
@@ -11,13 +11,13 @@
  */
 
 import type { ReactNode } from 'react';
-import type { AssistantMessage } from '../model/assistantContract';
+import type { AssistantAnswerMessage } from '../model/assistantContract';
 
 /** A tool the assistant called during a live run. `tool_call.done` may omit 
the name, so it is optional. */
 export interface AssistantToolActivity {
   id: string;
   name?: string;
-  status: 'running' | 'done';
+  status: 'running' | 'done' | 'unknown';
 }
 
 /**
@@ -30,7 +30,7 @@ export type AssistantAnswerErrorKind = 'failed' | 'stream' | 
'connection';
 export type AssistantAnswerView =
   | { status: 'loading' }
   | { status: 'streaming'; content: string; tools?: AssistantToolActivity[] }
-  | { status: 'done'; message: AssistantMessage }
+  | { status: 'done'; message: AssistantAnswerMessage }
   | {
       status: 'error';
       kind: AssistantAnswerErrorKind;

Reply via email to