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 b555ccbce fix(ai): preserve live answers when timeline refresh fails 
(#4867)
b555ccbce is described below

commit b555ccbce8952b50736b8868325cdea3256aca8c
Author: Hanabi <[email protected]>
AuthorDate: Thu Sep 24 18:19:29 2026 +0800

    fix(ai): preserve live answers when timeline refresh fails (#4867)
    
    fix(ai): preserve live answers when timeline refresh fails
---
 .../pages/ai/hooks/useAgentRun.timeline.test.ts    | 66 ++++++++++++++++++++++
 web/src/pages/ai/hooks/useConversationTimeline.ts  |  5 +-
 2 files changed, 69 insertions(+), 2 deletions(-)

diff --git a/web/src/pages/ai/hooks/useAgentRun.timeline.test.ts 
b/web/src/pages/ai/hooks/useAgentRun.timeline.test.ts
new file mode 100644
index 000000000..7b306882d
--- /dev/null
+++ b/web/src/pages/ai/hooks/useAgentRun.timeline.test.ts
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { act, renderHook, waitFor } from '@testing-library/react';
+import { expect, it, vi } from 'vitest';
+import { useAgentRun } from './useAgentRun';
+import { useConversationTimeline } from './useConversationTimeline';
+import { openRunStream, type RunStreamHandlers } from '../../../api/ai';
+import { getConversationTimeline } from '../../../api/aiConversations';
+
+vi.mock('../../../api/ai', () => ({ openRunStream: vi.fn(), attachRunStream: 
vi.fn() }));
+vi.mock('../../../api/aiConversations', () => ({
+  getConversationTimeline: vi.fn(),
+  stopRun: vi.fn(),
+  reportRunSpeed: vi.fn(),
+}));
+
+it('keepsTheLiveAnswerWhenTheRealTimelineRefreshFailsTest', async () => {
+  vi.mocked(getConversationTimeline)
+    .mockResolvedValueOnce({ items: [], nextAfter: null, activeRun: null })
+    .mockRejectedValueOnce(new Error('timeline unavailable'));
+  let handlers!: RunStreamHandlers;
+  let finish!: () => void;
+  vi.mocked(openRunStream).mockImplementation((_id, _body, callbacks) => {
+    handlers = callbacks;
+    return new Promise<void>((resolve) => {
+      finish = resolve;
+    });
+  });
+  const { result } = renderHook(() => {
+    const timeline = useConversationTimeline(7);
+    const run = useAgentRun(7, { refetchTimeline: timeline.refetch });
+    return { timeline, run };
+  });
+  await waitFor(() => expect(result.current.timeline.loading).toBe(false));
+  let sent!: Promise<void>;
+  await act(async () => {
+    sent = result.current.run.send(7, { message: 'hello' });
+  });
+  await act(async () => {
+    handlers.onEvent({ type: 'text_delta', content: 'answer not yet in 
history' });
+  });
+  const liveAnswer = result.current.run.blocksRef.current;
+  expect(liveAnswer.length).toBeGreaterThan(0);
+  await act(async () => {
+    finish();
+    await sent;
+  });
+  expect(result.current.timeline.error).toBe('timeline unavailable');
+  expect(result.current.run.blocksRef.current).toEqual(liveAnswer);
+  expect(result.current.run.error).toBe('timeline unavailable');
+});
diff --git a/web/src/pages/ai/hooks/useConversationTimeline.ts 
b/web/src/pages/ai/hooks/useConversationTimeline.ts
index bc900fd6a..33ae3adb0 100644
--- a/web/src/pages/ai/hooks/useConversationTimeline.ts
+++ b/web/src/pages/ai/hooks/useConversationTimeline.ts
@@ -87,7 +87,7 @@ export interface UseConversationTimelineResult {
   error: string;
   /** True when the bounded forward walk stopped before the tail; `loadMore` 
continues it. */
   hasMore: boolean;
-  /** Reload the whole transcript from `seq > 0`. Awaitable: `useAgentRun` 
depends on that. */
+  /** Reload the whole transcript from `seq > 0`; reject on failure so live 
blocks are retained. */
   refetch: () => Promise<void>;
   loadMore: () => Promise<void>;
 }
@@ -152,6 +152,7 @@ export function useConversationTimeline(
     } catch (loadError) {
       if (id !== requestId.current) return;
       setError(describeThrownMessage(loadError));
+      throw loadError;
     } finally {
       if (id === requestId.current) setLoading(false);
     }
@@ -188,7 +189,7 @@ export function useConversationTimeline(
   useEffect(() => {
     // Loading is asynchronous; state updates happen after the timeline API 
resolves.
     // eslint-disable-next-line react-hooks/set-state-in-effect
-    void refetch();
+    void refetch().catch(() => undefined);
     return () => {
       requestId.current += 1;
     };

Reply via email to