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 10a84ae19 fix(settings): discard stale model refreshes (#2744)
10a84ae19 is described below

commit 10a84ae19342a12e39c598b75310e517d4a31013
Author: btlqql <[email protected]>
AuthorDate: Wed Sep 2 17:10:43 2026 +0800

    fix(settings): discard stale model refreshes (#2744)
---
 web/src/pages/settings/AiAssistantTab.tsx          | 18 ++++++++--
 .../settings/__tests__/AiAssistantTab.test.tsx     | 39 ++++++++++++++++++++++
 2 files changed, 54 insertions(+), 3 deletions(-)

diff --git a/web/src/pages/settings/AiAssistantTab.tsx 
b/web/src/pages/settings/AiAssistantTab.tsx
index 3b7b16ce9..9e87e4d0c 100644
--- a/web/src/pages/settings/AiAssistantTab.tsx
+++ b/web/src/pages/settings/AiAssistantTab.tsx
@@ -117,6 +117,7 @@ export const AiAssistantTab = () => {
   const [modelOptions, setModelOptions] = useState<{ value: string; label: 
string }[]>([]);
   const [testResult, setTestResult] = useState<TestState | null>(null);
   const testRequestIdRef = useRef(0);
+  const configGenerationRef = useRef(0);
 
   const buildModelOptions = (
     nextProvider: string,
@@ -167,6 +168,7 @@ export const AiAssistantTab = () => {
     return () => {
       cancelled = true;
       testRequestIdRef.current += 1;
+      configGenerationRef.current += 1;
     };
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
@@ -177,6 +179,11 @@ export const AiAssistantTab = () => {
     setTestResult(null);
   };
 
+  const handleValuesChange = () => {
+    configGenerationRef.current += 1;
+    invalidateTestRequest();
+  };
+
   const handleProviderChange = (nextProvider: string) => {
     invalidateTestRequest();
     setModelOptions(fallbackModelOptions(nextProvider));
@@ -290,6 +297,7 @@ export const AiAssistantTab = () => {
   const handleSave = async () => {
     const payload = await buildPayload();
     if (!payload) return;
+    const configGeneration = configGenerationRef.current;
     setSaving(true);
     try {
       const result = await saveLlmConfig(payload);
@@ -302,9 +310,13 @@ export const AiAssistantTab = () => {
         try {
           const models = await getLlmModels();
           const remoteModels = models.data?.map((model) => model.id || 
'').filter(Boolean) ?? [];
-          setModelOptions(buildModelOptions(payload.provider, remoteModels, 
payload.model));
+          if (configGenerationRef.current === configGeneration) {
+            setModelOptions(buildModelOptions(payload.provider, remoteModels, 
payload.model));
+          }
         } catch {
-          message.warning(t('ai.modelsRefreshFailedAfterSave'));
+          if (configGenerationRef.current === configGeneration) {
+            message.warning(t('ai.modelsRefreshFailedAfterSave'));
+          }
         }
       } else {
         message.error(result.errMsg || t('settings.saveFailedShort'));
@@ -322,7 +334,7 @@ export const AiAssistantTab = () => {
         form={form}
         layout="vertical"
         initialValues={{ provider: 'tongyi', engine: 'claude-code' }}
-        onValuesChange={invalidateTestRequest}
+        onValuesChange={handleValuesChange}
       >
         <Flex vertical gap={24}>
           <Card
diff --git a/web/src/pages/settings/__tests__/AiAssistantTab.test.tsx 
b/web/src/pages/settings/__tests__/AiAssistantTab.test.tsx
index ecdc747fa..643d1012c 100644
--- a/web/src/pages/settings/__tests__/AiAssistantTab.test.tsx
+++ b/web/src/pages/settings/__tests__/AiAssistantTab.test.tsx
@@ -175,6 +175,45 @@ describe('AiAssistantTab', () => {
     ).toBeInTheDocument();
   });
 
+  it('ignores a saved model refresh after the provider changes', async () => {
+    localStorage.setItem(LANGUAGE_STORAGE_KEY, 'en');
+    let resolveModels!: (result: { status: number; data: { id: string }[] }) 
=> void;
+    const user = userEvent.setup();
+    llmApiMocks.getLlmModels
+      .mockResolvedValueOnce({ status: 0, data: [{ id: 'qwen3.8-max' }] })
+      .mockImplementationOnce(
+        () =>
+          new Promise((resolve) => {
+            resolveModels = resolve;
+          }),
+      );
+    renderPage();
+
+    await screen.findByText('API key configured');
+    await user.click(screen.getByRole('button', { name: 'Save' }));
+    await waitFor(() => 
expect(llmApiMocks.getLlmModels).toHaveBeenCalledTimes(2));
+
+    await user.click(screen.getAllByRole('combobox')[1]);
+    await user.click(
+      await screen.findByText('OpenAI', { selector: 
'.ant-select-item-option-content' }),
+    );
+    await act(async () =>
+      resolveModels({ status: 0, data: [{ id: 'stale-saved-provider-model' }] 
}),
+    );
+
+    const modelBox = screen.getAllByRole('combobox')[2];
+    await user.click(modelBox);
+    await user.clear(modelBox);
+    expect(
+      screen.queryByText('stale-saved-provider-model', {
+        selector: '.ant-select-item-option-content',
+      }),
+    ).not.toBeInTheDocument();
+    expect(
+      await screen.findByText('gpt-5.6-sol', { selector: 
'.ant-select-item-option-content' }),
+    ).toBeInTheDocument();
+  });
+
   it('refreshes the model list from a successful connection test', async () => 
{
     const user = userEvent.setup();
     llmApiMocks.testLlmConnection.mockResolvedValue({

Reply via email to