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({