@lobehub/lobehub 2.0.0-next.290 → 2.0.0-next.291

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,41 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.291](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.290...v2.0.0-next.291)
6
+
7
+ <sup>Released on **2026-01-15**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **settings**: Add instant UI feedback for provider config switches.
12
+ - **misc**: Click lobe ai topic trigger create new agent.
13
+
14
+ #### 💄 Styles
15
+
16
+ - **misc**: Improve agent loading state.
17
+
18
+ <br/>
19
+
20
+ <details>
21
+ <summary><kbd>Improvements and Fixes</kbd></summary>
22
+
23
+ #### What's fixed
24
+
25
+ - **settings**: Add instant UI feedback for provider config switches, closes [#11362](https://github.com/lobehub/lobe-chat/issues/11362) ([a758d01](https://github.com/lobehub/lobe-chat/commit/a758d01))
26
+ - **misc**: Click lobe ai topic trigger create new agent, closes [#11508](https://github.com/lobehub/lobe-chat/issues/11508) ([2443189](https://github.com/lobehub/lobe-chat/commit/2443189))
27
+
28
+ #### Styles
29
+
30
+ - **misc**: Improve agent loading state, closes [#11511](https://github.com/lobehub/lobe-chat/issues/11511) ([3bb7f33](https://github.com/lobehub/lobe-chat/commit/3bb7f33))
31
+
32
+ </details>
33
+
34
+ <div align="right">
35
+
36
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
37
+
38
+ </div>
39
+
5
40
  ## [Version 2.0.0-next.290](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.289...v2.0.0-next.290)
6
41
 
7
42
  <sup>Released on **2026-01-15**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Click lobe ai topic trigger create new agent."
6
+ ],
7
+ "improvements": [
8
+ "Improve agent loading state."
9
+ ]
10
+ },
11
+ "date": "2026-01-15",
12
+ "version": "2.0.0-next.291"
13
+ },
2
14
  {
3
15
  "children": {
4
16
  "fixes": [
@@ -203,6 +203,8 @@
203
203
  "noMembersYet": "This group doesn't have any members yet. Click the + button to invite agents.",
204
204
  "noSelectedAgents": "No members selected yet",
205
205
  "openInNewWindow": "Open in New Window",
206
+ "operation.execAgentRuntime": "Preparing response",
207
+ "operation.sendMessage": "Sending message",
206
208
  "owner": "Group owner",
207
209
  "pageCopilot.title": "Page Agent",
208
210
  "pageCopilot.welcome": "**Clearer, sharper writing**\n\nDraft, rewrite, or polish—tell me your intent and I’ll refine the rest.",
@@ -203,6 +203,8 @@
203
203
  "noMembersYet": "这个群组还没有成员。点击「+」邀请助理加入",
204
204
  "noSelectedAgents": "还未选择成员",
205
205
  "openInNewWindow": "在新窗口打开",
206
+ "operation.execAgentRuntime": "准备响应中",
207
+ "operation.sendMessage": "消息发送中",
206
208
  "owner": "群主",
207
209
  "pageCopilot.title": "文稿助理",
208
210
  "pageCopilot.welcome": "**让文字更清晰、更到位**\n\n起草、改写、润色都可以。你把意图说清楚,其余交给我打磨",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.290",
3
+ "version": "2.0.0-next.291",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -6,7 +6,7 @@ import { ModelIcon } from '@lobehub/icons';
6
6
  import { Alert, Button, Flexbox, Highlighter, Icon, Select } from '@lobehub/ui';
7
7
  import { cssVar } from 'antd-style';
8
8
  import { Loader2Icon } from 'lucide-react';
9
- import { type ReactNode, memo, useState } from 'react';
9
+ import { type ReactNode, memo, useEffect, useState } from 'react';
10
10
  import { useTranslation } from 'react-i18next';
11
11
 
12
12
  import { useProviderName } from '@/hooks/useProviderName';
@@ -58,9 +58,10 @@ const Checker = memo<ConnectionCheckerProps>(
58
58
  ({ model, provider, checkErrorRender: CheckErrorRender, onBeforeCheck, onAfterCheck }) => {
59
59
  const { t } = useTranslation('setting');
60
60
 
61
- const isProviderConfigUpdating = useAiInfraStore(
62
- aiProviderSelectors.isProviderConfigUpdating(provider),
63
- );
61
+ const [isProviderConfigUpdating, updateAiProviderConfig] = useAiInfraStore((s) => [
62
+ aiProviderSelectors.isProviderConfigUpdating(provider)(s),
63
+ s.updateAiProviderConfig,
64
+ ]);
64
65
  const totalModels = useAiInfraStore(aiModelSelectors.aiProviderChatModelListIds);
65
66
 
66
67
  const [loading, setLoading] = useState(false);
@@ -69,6 +70,11 @@ const Checker = memo<ConnectionCheckerProps>(
69
70
 
70
71
  const [error, setError] = useState<ChatMessageError | undefined>();
71
72
 
73
+ // Sync checkModel state when model prop changes
74
+ useEffect(() => {
75
+ setCheckModel(model);
76
+ }, [model]);
77
+
72
78
  const checkConnection = async () => {
73
79
  // Clear previous check results immediately
74
80
  setPass(false);
@@ -131,11 +137,14 @@ const Checker = memo<ConnectionCheckerProps>(
131
137
  <Select
132
138
  listItemHeight={36}
133
139
  onSelect={async (value) => {
134
- // Changing the check model should be a local UI concern only.
135
- // Persisting it to provider config would trigger global refresh/revalidation.
140
+ // Update local state
136
141
  setCheckModel(value);
137
142
  setPass(false);
138
143
  setError(undefined);
144
+
145
+ // Persist the selected model to provider config
146
+ // This allows the model to be retained after page refresh
147
+ await updateAiProviderConfig(provider, { checkModel: value });
139
148
  }}
140
149
  optionRender={({ value }) => {
141
150
  return (
@@ -177,9 +186,9 @@ const Checker = memo<ConnectionCheckerProps>(
177
186
  style={
178
187
  pass
179
188
  ? {
180
- borderColor: cssVar.colorSuccess,
181
- color: cssVar.colorSuccess,
182
- }
189
+ borderColor: cssVar.colorSuccess,
190
+ color: cssVar.colorSuccess,
191
+ }
183
192
  : undefined
184
193
  }
185
194
  >
@@ -13,7 +13,7 @@ import {
13
13
  } from '@lobehub/ui';
14
14
  import { Center, Flexbox, Skeleton } from '@lobehub/ui';
15
15
  import { useDebounceFn } from 'ahooks';
16
- import { Switch } from 'antd';
16
+ import { Form as AntdForm, Switch } from 'antd';
17
17
  import { createStaticStyles, cssVar, cx, responsive } from 'antd-style';
18
18
  import { Loader2Icon, LockIcon } from 'lucide-react';
19
19
  import Link from 'next/link';
@@ -150,26 +150,72 @@ const ProviderConfig = memo<ProviderConfigProps>(
150
150
  enabled,
151
151
  isLoading,
152
152
  configUpdating,
153
- enableResponseApi,
154
- isProviderEndpointNotEmpty,
155
- isProviderApiKeyNotEmpty,
153
+ providerRuntimeConfig,
156
154
  ] = useAiInfraStore((s) => [
157
155
  aiProviderSelectors.providerDetailById(id)(s),
158
156
  s.updateAiProviderConfig,
159
157
  aiProviderSelectors.isProviderEnabled(id)(s),
160
158
  aiProviderSelectors.isAiProviderConfigLoading(id)(s),
161
159
  aiProviderSelectors.isProviderConfigUpdating(id)(s),
162
- aiProviderSelectors.isProviderEnableResponseApi(id)(s),
163
- aiProviderSelectors.isActiveProviderEndpointNotEmpty(s),
164
- aiProviderSelectors.isActiveProviderApiKeyNotEmpty(s),
160
+ aiProviderSelectors.providerConfigById(id)(s),
165
161
  ]);
166
162
 
163
+ // Watch form values in real-time to show/hide switches immediately
164
+ // Watch nested form values for endpoints
165
+ const formBaseURL = AntdForm.useWatch(['keyVaults', 'baseURL'], form);
166
+ const formEndpoint = AntdForm.useWatch(['keyVaults', 'endpoint'], form);
167
+ // Watch all possible credential fields for different providers
168
+ const formApiKey = AntdForm.useWatch(['keyVaults', 'apiKey'], form);
169
+ const formAccessKeyId = AntdForm.useWatch(['keyVaults', 'accessKeyId'], form);
170
+ const formSecretAccessKey = AntdForm.useWatch(['keyVaults', 'secretAccessKey'], form);
171
+ const formUsername = AntdForm.useWatch(['keyVaults', 'username'], form);
172
+ const formPassword = AntdForm.useWatch(['keyVaults', 'password'], form);
173
+
174
+ // Check if provider has endpoint and apiKey based on runtime config
175
+ // Fallback to data.keyVaults if runtime config is not yet loaded
176
+ const keyVaults = providerRuntimeConfig?.keyVaults || data?.keyVaults;
177
+ // Use form values first (for immediate update), fallback to stored values
178
+ const isProviderEndpointNotEmpty =
179
+ !!formBaseURL || !!formEndpoint || !!keyVaults?.baseURL || !!keyVaults?.endpoint;
180
+ // Check if any credential is present for different authentication types:
181
+ // - Standard apiKey (OpenAI, Azure, Cloudflare, VertexAI, etc.)
182
+ // - AWS Bedrock credentials (accessKeyId, secretAccessKey)
183
+ // - ComfyUI basic auth (username and password)
184
+ const isProviderApiKeyNotEmpty = !!(
185
+ formApiKey ||
186
+ keyVaults?.apiKey ||
187
+ formAccessKeyId ||
188
+ keyVaults?.accessKeyId ||
189
+ formSecretAccessKey ||
190
+ keyVaults?.secretAccessKey ||
191
+ (formUsername && formPassword) ||
192
+ (keyVaults?.username && keyVaults?.password)
193
+ );
194
+
195
+ // Track the last initialized provider ID to avoid resetting form during edits
196
+ const lastInitializedIdRef = useRef<string | null>(null);
197
+
167
198
  useLayoutEffect(() => {
168
199
  if (isLoading) return;
169
200
 
170
- // set the first time
171
- form.setFieldsValue(data);
172
- }, [isLoading, id, data]);
201
+ // Only initialize form when:
202
+ // 1. First load (lastInitializedIdRef.current === null)
203
+ // 2. Provider ID changed (switching between providers)
204
+ const shouldInitialize = lastInitializedIdRef.current !== id;
205
+ if (!shouldInitialize) return;
206
+
207
+ // Merge data from both sources to ensure all fields are initialized correctly
208
+ // data: contains basic info like apiKey, baseURL, fetchOnClient
209
+ // providerRuntimeConfig: contains nested config like enableResponseApi
210
+ const mergedData = {
211
+ ...data,
212
+ ...(providerRuntimeConfig?.config && { config: providerRuntimeConfig.config }),
213
+ };
214
+
215
+ // Set form values and mark as initialized
216
+ form.setFieldsValue(mergedData);
217
+ lastInitializedIdRef.current = id;
218
+ }, [isLoading, id, data, providerRuntimeConfig, form]);
173
219
 
174
220
  // 标记是否正在进行连接测试
175
221
  const isCheckingConnection = useRef(false);
@@ -298,24 +344,23 @@ const ProviderConfig = memo<ProviderConfigProps>(
298
344
  (defaultShowBrowserRequest ||
299
345
  (showEndpoint && isProviderEndpointNotEmpty) ||
300
346
  (showApiKey && isProviderApiKeyNotEmpty));
301
- const clientFetchItem = showClientFetch && {
302
- children: isLoading ? <SkeletonSwitch /> : <Switch loading={configUpdating} />,
303
- desc: t('providerModels.config.fetchOnClient.desc'),
304
- label: t('providerModels.config.fetchOnClient.title'),
305
- minWidth: undefined,
306
- name: 'fetchOnClient',
307
- };
347
+
348
+ const clientFetchItem = showClientFetch
349
+ ? {
350
+ children: isLoading ? <SkeletonSwitch /> : <Switch loading={configUpdating} />,
351
+ desc: t('providerModels.config.fetchOnClient.desc'),
352
+ label: t('providerModels.config.fetchOnClient.title'),
353
+ minWidth: undefined,
354
+ name: 'fetchOnClient',
355
+ }
356
+ : undefined;
308
357
 
309
358
  const configItems = [
310
359
  ...apiKeyItem,
311
360
  endpointItem,
312
361
  supportResponsesApi
313
362
  ? {
314
- children: isLoading ? (
315
- <Skeleton.Button active />
316
- ) : (
317
- <Switch loading={configUpdating} value={enableResponseApi} />
318
- ),
363
+ children: isLoading ? <Skeleton.Button active /> : <Switch loading={configUpdating} />,
319
364
  desc: t('providerModels.config.responsesApi.desc'),
320
365
  label: t('providerModels.config.responsesApi.title'),
321
366
  minWidth: undefined,
@@ -364,7 +409,7 @@ const ProviderConfig = memo<ProviderConfigProps>(
364
409
 
365
410
  {isCustom && <UpdateProviderInfo />}
366
411
  {canDeactivate && !(ENABLE_BUSINESS_FEATURES && id === 'lobehub') && (
367
- <EnableSwitch id={id} />
412
+ <EnableSwitch id={id} key={id} />
368
413
  )}
369
414
  </Flexbox>
370
415
  ),
@@ -21,7 +21,7 @@ export const ChatInputProvider = memo<ChatInputProviderProps>(
21
21
  chatInputEditorRef,
22
22
  onMarkdownContentChange,
23
23
  mentionItems,
24
- allowExpand,
24
+ allowExpand = true,
25
25
  }) => {
26
26
  const editor = useEditor();
27
27
  const slashMenuRef = useRef<HTMLDivElement>(null);
@@ -8,7 +8,6 @@ import { CollapsedMessage } from '../../AssistantGroup/components/CollapsedMessa
8
8
  import DisplayContent from '../../components/DisplayContent';
9
9
  import FileChunks from '../../components/FileChunks';
10
10
  import ImageFileListViewer from '../../components/ImageFileListViewer';
11
- import IntentUnderstanding from '../../components/IntentUnderstanding';
12
11
  import Reasoning from '../../components/Reasoning';
13
12
  import SearchGrounding from '../../components/SearchGrounding';
14
13
  import { useMarkdown } from '../useMarkdown';
@@ -23,9 +22,6 @@ const MessageContent = memo<UIChatMessage>(
23
22
 
24
23
  const isToolCallGenerating = generating && (content === LOADING_FLAT || !content) && !!tools;
25
24
 
26
- // TODO: Need to implement isIntentUnderstanding selector in ConversationStore if needed
27
- const isIntentUnderstanding = false;
28
-
29
25
  const showSearch = !!search && !!search.citations?.length;
30
26
  const showImageItems = !!imageList && imageList.length > 0;
31
27
 
@@ -46,18 +42,15 @@ const MessageContent = memo<UIChatMessage>(
46
42
  )}
47
43
  {showFileChunks && <FileChunks data={chunksList} />}
48
44
  {showReasoning && <Reasoning {...props.reasoning} id={id} />}
49
- {isIntentUnderstanding ? (
50
- <IntentUnderstanding />
51
- ) : (
52
- <DisplayContent
53
- content={content}
54
- hasImages={showImageItems}
55
- isMultimodal={metadata?.isMultimodal}
56
- isToolCallGenerating={isToolCallGenerating}
57
- markdownProps={markdownProps}
58
- tempDisplayContent={metadata?.tempDisplayContent}
59
- />
60
- )}
45
+ <DisplayContent
46
+ content={content}
47
+ hasImages={showImageItems}
48
+ id={id}
49
+ isMultimodal={metadata?.isMultimodal}
50
+ isToolCallGenerating={isToolCallGenerating}
51
+ markdownProps={markdownProps}
52
+ tempDisplayContent={metadata?.tempDisplayContent}
53
+ />
61
54
  {showImageItems && <ImageFileListViewer items={imageList} />}
62
55
  </Flexbox>
63
56
  );
@@ -52,6 +52,7 @@ const MessageContent = memo<UIChatMessage>(
52
52
  <DisplayContent
53
53
  content={content}
54
54
  hasImages={showImageItems}
55
+ id={id}
55
56
  isMultimodal={metadata?.isMultimodal}
56
57
  isToolCallGenerating={isToolCallGenerating}
57
58
  markdownProps={markdownProps}
@@ -0,0 +1,64 @@
1
+ import { Flexbox, Text } from '@lobehub/ui';
2
+ import { memo, useEffect, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ import BubblesLoading from '@/components/BubblesLoading';
6
+ import { useChatStore } from '@/store/chat';
7
+ import { operationSelectors } from '@/store/chat/selectors';
8
+ import type { OperationType } from '@/store/chat/slices/operation/types';
9
+
10
+ const ELAPSED_TIME_THRESHOLD = 2100; // Show elapsed time after 2 seconds
11
+
12
+ interface ContentLoadingProps {
13
+ id: string;
14
+ }
15
+
16
+ const ContentLoading = memo<ContentLoadingProps>(({ id }) => {
17
+ const { t } = useTranslation('chat');
18
+ const operations = useChatStore(operationSelectors.getOperationsByMessage(id));
19
+ const [elapsedSeconds, setElapsedSeconds] = useState(0);
20
+
21
+ // Get the running operation
22
+ const runningOp = operations.find((op) => op.status === 'running');
23
+ const operationType = runningOp?.type as OperationType | undefined;
24
+ const startTime = runningOp?.metadata?.startTime;
25
+
26
+ // Track elapsed time, reset when operation type changes
27
+ useEffect(() => {
28
+ if (!startTime) {
29
+ setElapsedSeconds(0);
30
+ return;
31
+ }
32
+
33
+ const updateElapsed = () => {
34
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
35
+ setElapsedSeconds(elapsed);
36
+ };
37
+
38
+ updateElapsed();
39
+ const interval = setInterval(updateElapsed, 1000);
40
+
41
+ return () => clearInterval(interval);
42
+ }, [startTime, operationType]);
43
+
44
+ // Get localized label based on operation type
45
+ const operationLabel = operationType
46
+ ? (t(`operation.${operationType}` as any) as string)
47
+ : undefined;
48
+
49
+ const showElapsedTime = elapsedSeconds >= ELAPSED_TIME_THRESHOLD / 1000;
50
+
51
+ return (
52
+ <Flexbox align={'center'} horizontal>
53
+ <BubblesLoading />
54
+ {operationLabel && (
55
+ <Text type={'secondary'}>
56
+ {operationLabel}...
57
+ {showElapsedTime && ` (${elapsedSeconds}s)`}
58
+ </Text>
59
+ )}
60
+ </Flexbox>
61
+ );
62
+ });
63
+
64
+ export default ContentLoading;
@@ -2,17 +2,18 @@ import { deserializeParts } from '@lobechat/utils';
2
2
  import { type MarkdownProps } from '@lobehub/ui';
3
3
  import { memo } from 'react';
4
4
 
5
- import BubblesLoading from '@/components/BubblesLoading';
6
5
  import { LOADING_FLAT } from '@/const/message';
7
6
  import MarkdownMessage from '@/features/Conversation/Markdown';
8
7
 
9
8
  import { normalizeThinkTags, processWithArtifact } from '../../utils/markdown';
9
+ import ContentLoading from './ContentLoading';
10
10
  import { RichContentRenderer } from './RichContentRenderer';
11
11
 
12
12
  const DisplayContent = memo<{
13
13
  addIdOnDOM?: boolean;
14
14
  content: string;
15
15
  hasImages?: boolean;
16
+ id: string;
16
17
  isMultimodal?: boolean;
17
18
  isToolCallGenerating?: boolean;
18
19
  markdownProps?: Omit<MarkdownProps, 'className' | 'style' | 'children'>;
@@ -25,11 +26,12 @@ const DisplayContent = memo<{
25
26
  hasImages,
26
27
  isMultimodal,
27
28
  tempDisplayContent,
29
+ id,
28
30
  }) => {
29
31
  const message = normalizeThinkTags(processWithArtifact(content));
30
32
  if (isToolCallGenerating) return;
31
33
 
32
- if ((!content && !hasImages) || content === LOADING_FLAT) return <BubblesLoading />;
34
+ if ((!content && !hasImages) || content === LOADING_FLAT) return <ContentLoading id={id} />;
33
35
 
34
36
  const contentParts = isMultimodal ? deserializeParts(tempDisplayContent || content) : null;
35
37
 
@@ -41,10 +41,10 @@ const OllamaModelDownloader = memo<OllamaModelDownloaderProps>(
41
41
  isValidating: isDownloading,
42
42
  error,
43
43
  } = useActionSWR(
44
- [modelToPull],
45
- async ([model]) => {
44
+ ['ollama.downloadModel', modelToPull],
45
+ async () => {
46
46
  await modelsService.downloadModel(
47
- { model, provider: 'ollama' },
47
+ { model: modelToPull, provider: 'ollama' },
48
48
  { onProgress: handleProgress },
49
49
  );
50
50
 
@@ -1,5 +1,5 @@
1
1
  import useSWR, { type SWRHook } from 'swr';
2
-
2
+ import useSWRMutation from 'swr/mutation';
3
3
 
4
4
  /**
5
5
  * This type of request method is relatively flexible data, which will be triggered on the first time
@@ -63,20 +63,18 @@ export const useOnlyFetchOnceSWR: SWRHook = (key, fetch, config) =>
63
63
  });
64
64
 
65
65
  /**
66
- * 这一类请求方法用于做操作触发,必须使用 mutute 来触发请求操作,好处是自带了 loading / error 状态。
67
- * 可以很简单地完成 loading / error 态的交互处理,同时,相同 swr key 的请求会自动共享 loading态(例如新建助理按钮和右上角的 + 号)
66
+ * 这一类请求方法用于做操作触发,必须使用 mutate 来触发请求操作,好处是自带了 loading / error 状态。
67
+ * 可以很简单地完成 loading / error 态的交互处理,同时,相同 swr key 的请求会自动共享 loading 态(例如新建助理按钮和右上角的 + 号)
68
68
  * 非常适用于新建等操作。
69
+ *
70
+ * 使用 useSWRMutation 而非 useSWR,因为 useSWR 即使设置了 revalidateOnMount: false,
71
+ * 在缓存为空时仍会自动调用 fetcher。而 useSWRMutation 只会在手动调用 trigger 时执行。
69
72
  */
70
- // @ts-ignore
71
- export const useActionSWR: SWRHook = (key, fetch, config) =>
72
- useSWR(key, fetch, {
73
- refreshWhenHidden: false,
74
- refreshWhenOffline: false,
75
- revalidateOnFocus: false,
76
- revalidateOnMount: false,
77
- revalidateOnReconnect: false,
78
- ...config,
79
- });
73
+ export const useActionSWR = <T>(key: string | any[], fetcher: () => Promise<T>, config?: any) => {
74
+ const { trigger, isMutating, ...rest } = useSWRMutation(key, fetcher, config);
75
+ // Return with legacy property names for backward compatibility
76
+ return { ...rest, isValidating: isMutating, mutate: trigger };
77
+ };
80
78
 
81
79
  export interface SWRRefreshParams<T, A = (...args: any[]) => any> {
82
80
  action: A;
@@ -229,6 +229,8 @@ export default {
229
229
  'noMembersYet': "This group doesn't have any members yet. Click the + button to invite agents.",
230
230
  'noSelectedAgents': 'No members selected yet',
231
231
  'openInNewWindow': 'Open in New Window',
232
+ 'operation.execAgentRuntime': 'Preparing response',
233
+ 'operation.sendMessage': 'Sending message',
232
234
  'owner': 'Group owner',
233
235
  'pageCopilot.title': 'Page Agent',
234
236
  'pageCopilot.welcome':
@@ -77,10 +77,10 @@ export const normalizeImageModel = async (
77
77
  const fallbackParametersPromise = model.parameters
78
78
  ? Promise.resolve<ModelParamsSchema | undefined>(model.parameters)
79
79
  : getModelPropertyWithFallback<ModelParamsSchema | undefined>(
80
- model.id,
81
- 'parameters',
82
- model.providerId,
83
- );
80
+ model.id,
81
+ 'parameters',
82
+ model.providerId,
83
+ );
84
84
 
85
85
  const modelWithPricing = model as AIImageModelCard;
86
86
  const fallbackPricingPromise = modelWithPricing.pricing
@@ -260,6 +260,19 @@ export const createAiProviderSlice: StateCreator<
260
260
  toggleProviderEnabled: async (id: string, enabled: boolean) => {
261
261
  get().internal_toggleAiProviderLoading(id, true);
262
262
  await aiProviderService.toggleProviderEnabled(id, enabled);
263
+
264
+ // Immediately update local aiProviderList to reflect the change
265
+ // This ensures the switch displays correctly without waiting for SWR refresh
266
+ set(
267
+ (state) => ({
268
+ aiProviderList: state.aiProviderList.map((item) =>
269
+ item.id === id ? { ...item, enabled } : item,
270
+ ),
271
+ }),
272
+ false,
273
+ 'toggleProviderEnabled/syncEnabled',
274
+ );
275
+
263
276
  await get().refreshAiProviderList();
264
277
 
265
278
  get().internal_toggleAiProviderLoading(id, false);
@@ -277,6 +290,61 @@ export const createAiProviderSlice: StateCreator<
277
290
  updateAiProviderConfig: async (id, value) => {
278
291
  get().internal_toggleAiProviderConfigUpdating(id, true);
279
292
  await aiProviderService.updateAiProviderConfig(id, value);
293
+
294
+ // Immediately update local state for instant UI feedback
295
+ set(
296
+ (state) => {
297
+ const currentRuntimeConfig = state.aiProviderRuntimeConfig[id];
298
+ const currentDetailConfig = state.aiProviderDetailMap[id];
299
+
300
+ const updates: Partial<typeof currentRuntimeConfig> = {};
301
+ const detailUpdates: Partial<typeof currentDetailConfig> = {};
302
+
303
+ // Update fetchOnClient if changed
304
+ if (typeof value.fetchOnClient !== 'undefined') {
305
+ // Convert null to undefined to match the interface definition
306
+ const fetchOnClientValue = value.fetchOnClient === null ? undefined : value.fetchOnClient;
307
+ updates.fetchOnClient = fetchOnClientValue;
308
+ detailUpdates.fetchOnClient = fetchOnClientValue;
309
+ }
310
+
311
+ // Update config.enableResponseApi if changed
312
+ if (value.config?.enableResponseApi !== undefined && currentRuntimeConfig?.config) {
313
+ updates.config = {
314
+ ...currentRuntimeConfig.config,
315
+ enableResponseApi: value.config.enableResponseApi,
316
+ };
317
+ }
318
+
319
+ return {
320
+ // Update detail map for form display
321
+ aiProviderDetailMap:
322
+ currentDetailConfig && Object.keys(detailUpdates).length > 0
323
+ ? {
324
+ ...state.aiProviderDetailMap,
325
+ [id]: {
326
+ ...currentDetailConfig,
327
+ ...detailUpdates,
328
+ },
329
+ }
330
+ : state.aiProviderDetailMap,
331
+ // Update runtime config for selectors
332
+ aiProviderRuntimeConfig:
333
+ currentRuntimeConfig && Object.keys(updates).length > 0
334
+ ? {
335
+ ...state.aiProviderRuntimeConfig,
336
+ [id]: {
337
+ ...currentRuntimeConfig,
338
+ ...updates,
339
+ },
340
+ }
341
+ : state.aiProviderRuntimeConfig,
342
+ };
343
+ },
344
+ false,
345
+ 'updateAiProviderConfig/syncChanges',
346
+ );
347
+
280
348
  await get().refreshAiProviderDetail();
281
349
 
282
350
  get().internal_toggleAiProviderConfigUpdating(id, false);
@@ -204,8 +204,9 @@ export const conversationLifecycle: StateCreator<
204
204
  );
205
205
  get().internal_toggleMessageLoading(true, tempId);
206
206
 
207
- // Associate temp message with operation
207
+ // Associate temp messages with operation
208
208
  get().associateMessageWithOperation(tempId, operationId);
209
+ get().associateMessageWithOperation(tempAssistantId, operationId);
209
210
 
210
211
  // Store editor state in operation metadata for cancel restoration
211
212
  const jsonState = mainInputEditor?.getJSONState();