@nextclaw/ui 0.12.33 → 0.12.35-beta.0

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.
Files changed (133) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/assets/{api-rASx2-N4.js → api-De1PAch0.js} +3 -3
  3. package/dist/assets/{book-open-BcnAiKde.js → book-open-825Pcd0z.js} +1 -1
  4. package/dist/assets/channels-list-page-CGjXDSD-.js +8 -0
  5. package/dist/assets/chat-page-RsUTK_bY.js +105 -0
  6. package/dist/assets/{config-split-page-CcrEUtwu.js → config-split-page-BvfxFQSb.js} +1 -1
  7. package/dist/assets/{createLucideIcon-DzY6wN61.js → createLucideIcon-CeNHY6BW.js} +1 -1
  8. package/dist/assets/desktop-update-config-B_YIv5ZJ.js +1 -0
  9. package/dist/assets/{dist-NBjcwlsw.js → dist-uH7tiW6E.js} +1 -1
  10. package/dist/assets/{doc-browser-D2pq4IHR.js → doc-browser-CDe9yz9n.js} +1 -1
  11. package/dist/assets/doc-browser-DwT64GuF.js +1 -0
  12. package/dist/assets/{doc-browser-context-BPeeiNJ_.js → doc-browser-context-CQAx_92e.js} +1 -1
  13. package/dist/assets/{es2015-CVAuMqlM.js → es2015-c9XYpli5.js} +1 -1
  14. package/dist/assets/{external-link-CpEvG65F.js → external-link-BE1deZ_c.js} +1 -1
  15. package/dist/assets/i18n-DnubSHoK.js +1 -0
  16. package/dist/assets/index-6WZ6JjaU.css +1 -0
  17. package/dist/assets/index-BjH_p3R6.js +103 -0
  18. package/dist/assets/{key-round-DUq47t0P.js → key-round-CHo3gqpO.js} +1 -1
  19. package/dist/assets/mcp-marketplace-page-0C214hGi.js +40 -0
  20. package/dist/assets/mcp-marketplace-page-amWcMncu.js +1 -0
  21. package/dist/assets/model-config-B8zNrkag.js +1 -0
  22. package/dist/assets/notice-card-DO33_LzD.js +5 -0
  23. package/dist/assets/play-CqlWbVNo.js +1 -0
  24. package/dist/assets/plus-p61tQh2-.js +1 -0
  25. package/dist/assets/provider-scoped-model-input-B0_r_4EY.js +1 -0
  26. package/dist/assets/providers-list-B6v3foi7.js +1 -0
  27. package/dist/assets/remote-k671rLmY.js +1 -0
  28. package/dist/assets/{rotate-cw-BN9yjccP.js → rotate-cw-CYUByuqo.js} +1 -1
  29. package/dist/assets/runtime-config-page-DgH-XJwK.js +1 -0
  30. package/dist/assets/{save-CO_4qf6b.js → save-B6Ga8JIq.js} +1 -1
  31. package/dist/assets/{search-CRtQwr-h.js → search-5kKpgNoP.js} +1 -1
  32. package/dist/assets/search-config-DPZ6d9y6.js +1 -0
  33. package/dist/assets/{secrets-config-DFbHCb9n.js → secrets-config-CrXA11bo.js} +2 -2
  34. package/dist/assets/{select-HI5kiwX_.js → select-C0hGOCOK.js} +1 -1
  35. package/dist/assets/{setting-row-B4QnghjV.js → setting-row-xsM_0nTd.js} +1 -1
  36. package/dist/assets/{tabs-custom-D48zdZoc.js → tabs-custom-Bnqfhe4p.js} +1 -1
  37. package/dist/assets/{tag-chip-BPgMfp9l.js → tag-chip-cktJJMzh.js} +1 -1
  38. package/dist/assets/x-DDQw-C2x.js +1 -0
  39. package/dist/index.html +17 -29
  40. package/package.json +9 -9
  41. package/src/app/components/layout/app-layout.test.tsx +49 -4
  42. package/src/app/components/layout/app-layout.tsx +4 -7
  43. package/src/app/components/layout/settings-entry-page.test.tsx +9 -0
  44. package/src/app/components/layout/sidebar.layout.test.tsx +2 -2
  45. package/src/app/components/layout/sidebar.tsx +3 -3
  46. package/src/app/configs/app-navigation.config.ts +0 -12
  47. package/src/app/hooks/use-app-event-consumers.ts +1 -6
  48. package/src/app/hooks/use-viewport-layout.ts +0 -1
  49. package/src/app/index.tsx +2 -10
  50. package/src/features/channels/components/config/channel-form.tsx +85 -47
  51. package/src/features/channels/components/config/weixin-channel-auth-section.test.tsx +45 -1
  52. package/src/features/channels/components/config/weixin-channel-auth-section.tsx +119 -1
  53. package/src/features/channels/hooks/use-channel-auth.ts +8 -1
  54. package/src/features/channels/index.ts +1 -1
  55. package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +2 -2
  56. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +2 -7
  57. package/src/features/chat/components/layout/chat-sidebar.test.tsx +27 -3
  58. package/src/features/chat/components/layout/chat-sidebar.tsx +2 -3
  59. package/src/features/chat/managers/chat-session-list.manager.test.ts +12 -11
  60. package/src/features/chat/managers/chat-session-list.manager.ts +2 -1
  61. package/src/features/chat/managers/chat-ui.manager.ts +2 -2
  62. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +3 -3
  63. package/src/features/chat/pages/ncp-chat-page.test.ts +3 -0
  64. package/src/features/chat/pages/ncp-chat-page.tsx +19 -19
  65. package/src/features/chat/utils/chat-session-route.utils.test.ts +14 -0
  66. package/src/features/chat/utils/chat-session-route.utils.ts +3 -1
  67. package/src/features/chat/utils/ncp-chat-runtime-availability.utils.test.ts +6 -6
  68. package/src/features/marketplace/components/marketplace-list-card.tsx +4 -36
  69. package/src/features/marketplace/components/marketplace-page-detail.test.tsx +0 -1
  70. package/src/features/marketplace/components/marketplace-page.test.tsx +9 -128
  71. package/src/features/marketplace/components/marketplace-page.tsx +16 -72
  72. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +1 -1
  73. package/src/features/marketplace/hooks/use-marketplace.ts +1 -10
  74. package/src/features/marketplace/utils/marketplace-installed-cache.utils.test.ts +23 -21
  75. package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +12 -12
  76. package/src/features/system-status/managers/system-status.manager.test.ts +6 -6
  77. package/src/index.css +1 -1
  78. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +29 -13
  79. package/src/platforms/desktop/components/desktop-app-shell.tsx +14 -16
  80. package/src/platforms/desktop/components/desktop-window-chrome.tsx +2 -3
  81. package/src/platforms/mobile/components/mobile-app-shell.tsx +3 -1
  82. package/src/platforms/mobile/components/mobile-topbar.tsx +6 -5
  83. package/src/platforms/mobile/index.ts +1 -0
  84. package/src/shared/components/config/provider-form-support.ts +51 -33
  85. package/src/shared/components/config/provider-form.tsx +68 -35
  86. package/src/shared/components/config/provider-models-section.tsx +19 -4
  87. package/src/shared/components/model-config.test.tsx +2 -0
  88. package/src/shared/components/ui/button.tsx +1 -1
  89. package/src/shared/lib/api/channel-auth.types.ts +8 -0
  90. package/src/shared/lib/api/types.ts +17 -20
  91. package/src/shared/lib/api/utils/channel-auth.utils.ts +10 -1
  92. package/src/shared/lib/api/utils/marketplace.utils.ts +0 -5
  93. package/src/shared/lib/i18n/channel-auth.constants.ts +24 -10
  94. package/src/shared/lib/i18n/chat-labels.utils.ts +2 -2
  95. package/src/shared/lib/i18n/index.ts +2 -0
  96. package/src/shared/lib/i18n/marketplace.ts +0 -16
  97. package/src/shared/lib/provider-models/index.ts +41 -13
  98. package/src/shared/lib/ui-document-title/index.ts +0 -1
  99. package/dist/assets/app-manager-provider-CUkcbdWk.js +0 -1
  100. package/dist/assets/channels-list-page-ZWYqfcCe.js +0 -8
  101. package/dist/assets/chat-page-ACm58eUO.js +0 -1
  102. package/dist/assets/cpu-DKc3bQic.js +0 -3
  103. package/dist/assets/desktop-C-cDRCBY.js +0 -3
  104. package/dist/assets/desktop-update-config-DAD9JN9Q.js +0 -1
  105. package/dist/assets/dialog-CR80q3Rm.js +0 -5
  106. package/dist/assets/doc-browser-DhIkcy0-.js +0 -1
  107. package/dist/assets/doc-browser-fLs0TcdS.js +0 -1
  108. package/dist/assets/download-CMM8po31.js +0 -1
  109. package/dist/assets/i18n-D1144VAA.js +0 -1
  110. package/dist/assets/index-DV57tBmg.css +0 -1
  111. package/dist/assets/index-Db9Cufsy.js +0 -100
  112. package/dist/assets/marketplace-page-CAp_gpNe.js +0 -105
  113. package/dist/assets/marketplace-page-PPY5uMHK.js +0 -1
  114. package/dist/assets/mcp-marketplace-page-87__92b1.js +0 -1
  115. package/dist/assets/mcp-marketplace-page-C6HENB6V.js +0 -40
  116. package/dist/assets/model-config-CK3a8DWH.js +0 -1
  117. package/dist/assets/notice-card-CF4AJPM3.js +0 -1
  118. package/dist/assets/play-AqrNslHI.js +0 -1
  119. package/dist/assets/plus-B-YHtTNC.js +0 -1
  120. package/dist/assets/popover-DeAliE_4.js +0 -1
  121. package/dist/assets/provider-scoped-model-input-B8LJ6RCt.js +0 -1
  122. package/dist/assets/providers-list-BTPjqNdK.js +0 -1
  123. package/dist/assets/refresh-cw-CrbD8EkT.js +0 -1
  124. package/dist/assets/remote-DiPWGRZc.js +0 -1
  125. package/dist/assets/runtime-config-page-Bb9he7t8.js +0 -1
  126. package/dist/assets/search-config-BkbSXa9d.js +0 -1
  127. package/dist/assets/settings-MWL2SMyk.js +0 -1
  128. package/dist/assets/sparkles-BmgOD4nY.js +0 -1
  129. package/dist/assets/status-dot-l3kPFdq_.js +0 -1
  130. package/dist/assets/use-config-BFqzVF8Y.js +0 -1
  131. package/dist/assets/use-infinite-scroll-loader-DXhw9W4l.js +0 -1
  132. package/dist/assets/x-BeyYA_h6.js +0 -1
  133. /package/dist/assets/{config-hints-BNfpOL4J.js → config-hints-DGfwbumO.js} +0 -0
@@ -3,7 +3,10 @@ import type { getLanguage } from '@/shared/lib/i18n';
3
3
  import type { ProviderConfigUpdate, ProviderConfigView, ThinkingLevel } from '@/shared/lib/api';
4
4
 
5
5
  type WireApiType = 'auto' | 'chat' | 'responses';
6
- type ModelThinkingConfig = Record<string, { supported: ThinkingLevel[]; default?: ThinkingLevel | null }>;
6
+ type ModelConfig = Record<string, {
7
+ thinking?: { supported: ThinkingLevel[]; default?: ThinkingLevel | null };
8
+ vision?: boolean;
9
+ }>;
7
10
  type ProviderAuthMethodOption = {
8
11
  id: string;
9
12
  };
@@ -17,7 +20,7 @@ const EMPTY_PROVIDER_CONFIG: ProviderConfigView = {
17
20
  extraHeaders: null,
18
21
  wireApi: null,
19
22
  models: [],
20
- modelThinking: {}
23
+ modelConfig: {}
21
24
  };
22
25
 
23
26
  const THINKING_LEVELS: ThinkingLevel[] = ['off', 'minimal', 'low', 'medium', 'high', 'adaptive', 'xhigh'];
@@ -144,53 +147,65 @@ function normalizeThinkingLevels(values: unknown): ThinkingLevel[] {
144
147
  return deduped;
145
148
  }
146
149
 
147
- function normalizeModelThinkingConfig(
148
- input: ProviderConfigView['modelThinking'],
150
+ function normalizeModelConfig(
151
+ input: ProviderConfigView['modelConfig'],
149
152
  aliases: string[]
150
- ): ModelThinkingConfig {
153
+ ): ModelConfig {
151
154
  if (!input) {
152
155
  return {};
153
156
  }
154
- const normalized: ModelThinkingConfig = {};
157
+ const normalized: ModelConfig = {};
155
158
  for (const [rawModel, rawValue] of Object.entries(input)) {
156
159
  const model = toProviderLocalModelId(rawModel, aliases);
157
160
  if (!model) {
158
161
  continue;
159
162
  }
160
- const supported = normalizeThinkingLevels(rawValue?.supported);
161
- if (supported.length === 0) {
162
- continue;
163
+ const entry: NonNullable<ModelConfig[string]> = {};
164
+ const supported = normalizeThinkingLevels(rawValue?.thinking?.supported);
165
+ if (supported.length > 0) {
166
+ const defaultLevel = parseThinkingLevel(rawValue?.thinking?.default);
167
+ entry.thinking =
168
+ defaultLevel && supported.includes(defaultLevel)
169
+ ? { supported, default: defaultLevel }
170
+ : { supported };
171
+ }
172
+ if (rawValue?.vision === true) {
173
+ entry.vision = true;
174
+ }
175
+ if (entry.thinking || entry.vision === true) {
176
+ normalized[model] = entry;
163
177
  }
164
- const defaultLevel = parseThinkingLevel(rawValue?.default);
165
- normalized[model] =
166
- defaultLevel && supported.includes(defaultLevel)
167
- ? { supported, default: defaultLevel }
168
- : { supported };
169
178
  }
170
179
  return normalized;
171
180
  }
172
181
 
173
- function normalizeModelThinkingForModels(modelThinking: ModelThinkingConfig, models: string[]): ModelThinkingConfig {
182
+ function normalizeModelConfigForModels(modelConfig: ModelConfig, models: string[]): ModelConfig {
174
183
  const modelSet = new Set(models.map((item) => item.trim()).filter(Boolean));
175
- const normalized: ModelThinkingConfig = {};
176
- for (const [model, entry] of Object.entries(modelThinking)) {
184
+ const normalized: ModelConfig = {};
185
+ for (const [model, entry] of Object.entries(modelConfig)) {
177
186
  if (!modelSet.has(model)) {
178
187
  continue;
179
188
  }
180
- const supported = normalizeThinkingLevels(entry.supported);
181
- if (supported.length === 0) {
182
- continue;
189
+ const nextEntry: NonNullable<ModelConfig[string]> = {};
190
+ const supported = normalizeThinkingLevels(entry.thinking?.supported);
191
+ if (supported.length > 0) {
192
+ const defaultLevel = parseThinkingLevel(entry.thinking?.default);
193
+ nextEntry.thinking =
194
+ defaultLevel && supported.includes(defaultLevel)
195
+ ? { supported, default: defaultLevel }
196
+ : { supported };
197
+ }
198
+ if (entry.vision === true) {
199
+ nextEntry.vision = true;
200
+ }
201
+ if (nextEntry.thinking || nextEntry.vision === true) {
202
+ normalized[model] = nextEntry;
183
203
  }
184
- const defaultLevel = parseThinkingLevel(entry.default);
185
- normalized[model] =
186
- defaultLevel && supported.includes(defaultLevel)
187
- ? { supported, default: defaultLevel }
188
- : { supported };
189
204
  }
190
205
  return normalized;
191
206
  }
192
207
 
193
- function modelThinkingEqual(left: ModelThinkingConfig, right: ModelThinkingConfig): boolean {
208
+ function modelConfigEqual(left: ModelConfig, right: ModelConfig): boolean {
194
209
  const leftKeys = Object.keys(left).sort();
195
210
  const rightKeys = Object.keys(right).sort();
196
211
  if (leftKeys.length !== rightKeys.length) {
@@ -206,12 +221,15 @@ function modelThinkingEqual(left: ModelThinkingConfig, right: ModelThinkingConfi
206
221
  if (!leftEntry || !rightEntry) {
207
222
  return false;
208
223
  }
209
- const leftSupported = [...leftEntry.supported].sort();
210
- const rightSupported = [...rightEntry.supported].sort();
224
+ const leftSupported = [...(leftEntry.thinking?.supported ?? [])].sort();
225
+ const rightSupported = [...(rightEntry.thinking?.supported ?? [])].sort();
211
226
  if (!modelListsEqual(leftSupported, rightSupported)) {
212
227
  return false;
213
228
  }
214
- if ((leftEntry.default ?? null) !== (rightEntry.default ?? null)) {
229
+ if ((leftEntry.thinking?.default ?? null) !== (rightEntry.thinking?.default ?? null)) {
230
+ return false;
231
+ }
232
+ if ((leftEntry.vision ?? false) !== (rightEntry.vision ?? false)) {
215
233
  return false;
216
234
  }
217
235
  }
@@ -304,18 +322,18 @@ function shouldUsePillSelector(params: {
304
322
  return params.required && params.hasDefault && params.optionCount > 1 && params.optionCount <= 3;
305
323
  }
306
324
 
307
- export type { ModelThinkingConfig, ProviderAuthMethodOption, WireApiType };
325
+ export type { ModelConfig, ProviderAuthMethodOption, WireApiType };
308
326
  export {
309
327
  applyEnabledPatch,
310
328
  EMPTY_PROVIDER_CONFIG,
311
329
  formatThinkingLevelLabel,
312
330
  headersEqual,
313
331
  modelListsEqual,
314
- modelThinkingEqual,
332
+ modelConfigEqual,
315
333
  normalizeHeaders,
316
334
  normalizeModelList,
317
- normalizeModelThinkingConfig,
318
- normalizeModelThinkingForModels,
335
+ normalizeModelConfig,
336
+ normalizeModelConfigForModels,
319
337
  resolveEditableModels,
320
338
  resolvePreferredAuthMethodId,
321
339
  serializeModelsForSave,
@@ -36,18 +36,18 @@ import {
36
36
  formatThinkingLevelLabel,
37
37
  headersEqual,
38
38
  modelListsEqual,
39
- modelThinkingEqual,
39
+ modelConfigEqual,
40
40
  normalizeHeaders,
41
41
  normalizeModelList,
42
- normalizeModelThinkingConfig,
43
- normalizeModelThinkingForModels,
42
+ normalizeModelConfig,
43
+ normalizeModelConfigForModels,
44
44
  resolveEditableModels,
45
45
  resolvePreferredAuthMethodId,
46
46
  serializeModelsForSave,
47
47
  shouldUsePillSelector,
48
48
  THINKING_LEVELS,
49
49
  toProviderLocalModelId,
50
- type ModelThinkingConfig,
50
+ type ModelConfig,
51
51
  type WireApiType
52
52
  } from './provider-form-support';
53
53
  import { ProviderModelsSection } from './provider-models-section';
@@ -77,7 +77,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
77
77
  const [extraHeaders, setExtraHeaders] = useState<Record<string, string> | null>(null);
78
78
  const [wireApi, setWireApi] = useState<WireApiType>('auto');
79
79
  const [models, setModels] = useState<string[]>([]);
80
- const [modelThinking, setModelThinking] = useState<ModelThinkingConfig>({});
80
+ const [modelConfig, setModelConfig] = useState<ModelConfig>({});
81
81
  const [modelDraft, setModelDraft] = useState('');
82
82
  const [providerDisplayName, setProviderDisplayName] = useState('');
83
83
  const [showAdvanced, setShowAdvanced] = useState(false);
@@ -124,13 +124,13 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
124
124
  [resolvedProviderConfig.models, providerModelAliases]
125
125
  );
126
126
  const currentEditableModels = useMemo(() => resolveEditableModels(defaultModels, currentModels), [defaultModels, currentModels]);
127
- const currentModelThinking = useMemo(
127
+ const currentModelConfig = useMemo(
128
128
  () =>
129
- normalizeModelThinkingForModels(
130
- normalizeModelThinkingConfig(resolvedProviderConfig.modelThinking, providerModelAliases),
129
+ normalizeModelConfigForModels(
130
+ normalizeModelConfig(resolvedProviderConfig.modelConfig ?? providerSpec?.modelConfig, providerModelAliases),
131
131
  currentEditableModels
132
132
  ),
133
- [currentEditableModels, providerModelAliases, resolvedProviderConfig.modelThinking]
133
+ [currentEditableModels, providerModelAliases, providerSpec?.modelConfig, resolvedProviderConfig.modelConfig]
134
134
  );
135
135
  const language = getLanguage();
136
136
  const apiBaseHelpText =
@@ -235,7 +235,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
235
235
  setExtraHeaders(null);
236
236
  setWireApi('auto');
237
237
  setModels([]);
238
- setModelThinking({});
238
+ setModelConfig({});
239
239
  setModelDraft('');
240
240
  setProviderDisplayName('');
241
241
  setAuthSessionId(null);
@@ -251,7 +251,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
251
251
  setExtraHeaders(resolvedProviderConfig.extraHeaders || null);
252
252
  setWireApi(currentWireApi);
253
253
  setModels(currentEditableModels);
254
- setModelThinking(currentModelThinking);
254
+ setModelConfig(currentModelConfig);
255
255
  setModelDraft('');
256
256
  setProviderDisplayName(effectiveDisplayName);
257
257
  setAuthSessionId(null);
@@ -265,14 +265,14 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
265
265
  resolvedProviderConfig.extraHeaders,
266
266
  currentWireApi,
267
267
  currentEditableModels,
268
- currentModelThinking,
268
+ currentModelConfig,
269
269
  effectiveDisplayName,
270
270
  preferredAuthMethodId,
271
271
  clearAuthPollTimer
272
272
  ]);
273
273
 
274
274
  useEffect(() => () => clearAuthPollTimer(), [clearAuthPollTimer]);
275
- useEffect(() => setModelThinking((prev) => normalizeModelThinkingForModels(prev, models)), [models]);
275
+ useEffect(() => setModelConfig((prev) => normalizeModelConfigForModels(prev, models)), [models]);
276
276
 
277
277
  const hasChanges = useMemo(() => {
278
278
  if (!providerName) {
@@ -285,7 +285,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
285
285
  !headersEqual(extraHeaders, currentHeaders) ||
286
286
  (supportsWireApi && wireApi !== currentWireApi) ||
287
287
  !modelListsEqual(models, currentEditableModels) ||
288
- !modelThinkingEqual(modelThinking, currentModelThinking) ||
288
+ !modelConfigEqual(modelConfig, currentModelConfig) ||
289
289
  (isCustomProvider && providerDisplayName.trim() !== effectiveDisplayName)
290
290
  );
291
291
  }, [
@@ -302,8 +302,8 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
302
302
  currentWireApi,
303
303
  models,
304
304
  currentEditableModels,
305
- modelThinking,
306
- currentModelThinking,
305
+ modelConfig,
306
+ currentModelConfig,
307
307
  isCustomProvider,
308
308
  providerDisplayName,
309
309
  effectiveDisplayName
@@ -323,42 +323,74 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
323
323
  };
324
324
 
325
325
  const toggleModelThinkingLevel = (modelName: string, level: ThinkingLevel) => {
326
- setModelThinking((prev) => {
326
+ setModelConfig((prev) => {
327
327
  const currentEntry = prev[modelName];
328
- const currentLevels = currentEntry?.supported ?? [];
328
+ const currentLevels = currentEntry?.thinking?.supported ?? [];
329
329
  const nextLevels = currentLevels.includes(level)
330
330
  ? currentLevels.filter((item) => item !== level)
331
331
  : THINKING_LEVELS.filter((item) => item === level || currentLevels.includes(item));
332
- if (nextLevels.length === 0) {
333
- const next = { ...prev };
334
- delete next[modelName];
335
- return next;
336
- }
337
332
  const nextDefault =
338
- currentEntry?.default && nextLevels.includes(currentEntry.default) ? currentEntry.default : undefined;
339
- return {
340
- ...prev,
341
- [modelName]: nextDefault ? { supported: nextLevels, default: nextDefault } : { supported: nextLevels }
333
+ currentEntry?.thinking?.default && nextLevels.includes(currentEntry.thinking.default)
334
+ ? currentEntry.thinking.default
335
+ : undefined;
336
+ const nextEntry = {
337
+ ...currentEntry,
338
+ thinking:
339
+ nextLevels.length > 0
340
+ ? nextDefault
341
+ ? { supported: nextLevels, default: nextDefault }
342
+ : { supported: nextLevels }
343
+ : undefined
342
344
  };
345
+ const next = { ...prev };
346
+ if (nextEntry.thinking || nextEntry.vision === true) {
347
+ next[modelName] = nextEntry;
348
+ } else {
349
+ delete next[modelName];
350
+ }
351
+ return next;
343
352
  });
344
353
  };
345
354
 
346
355
  const setModelThinkingDefault = (modelName: string, level: ThinkingLevel | null) => {
347
- setModelThinking((prev) => {
356
+ setModelConfig((prev) => {
348
357
  const currentEntry = prev[modelName];
349
- if (!currentEntry || currentEntry.supported.length === 0) {
358
+ const currentThinking = currentEntry?.thinking;
359
+ if (!currentThinking || currentThinking.supported.length === 0) {
350
360
  return prev;
351
361
  }
352
- if (level && !currentEntry.supported.includes(level)) {
362
+ if (level && !currentThinking.supported.includes(level)) {
353
363
  return prev;
354
364
  }
355
365
  return {
356
366
  ...prev,
357
- [modelName]: level ? { supported: currentEntry.supported, default: level } : { supported: currentEntry.supported }
367
+ [modelName]: {
368
+ ...currentEntry,
369
+ thinking: level
370
+ ? { supported: currentThinking.supported, default: level }
371
+ : { supported: currentThinking.supported }
372
+ }
358
373
  };
359
374
  });
360
375
  };
361
376
 
377
+ const setModelVision = (modelName: string, vision: boolean) => {
378
+ setModelConfig((prev) => {
379
+ const currentEntry = prev[modelName];
380
+ const nextEntry = {
381
+ ...currentEntry,
382
+ vision: vision ? true : undefined
383
+ };
384
+ const next = { ...prev };
385
+ if (nextEntry.thinking || nextEntry.vision === true) {
386
+ next[modelName] = nextEntry;
387
+ } else {
388
+ delete next[modelName];
389
+ }
390
+ return next;
391
+ });
392
+ };
393
+
362
394
  const handleSubmit = (e: React.FormEvent) => {
363
395
  e.preventDefault();
364
396
  if (!providerName) {
@@ -390,8 +422,8 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
390
422
  if (!modelListsEqual(models, currentEditableModels)) {
391
423
  payload.models = serializeModelsForSave(models, defaultModels);
392
424
  }
393
- if (!modelThinkingEqual(modelThinking, currentModelThinking)) {
394
- payload.modelThinking = normalizeModelThinkingForModels(modelThinking, models);
425
+ if (!modelConfigEqual(modelConfig, currentModelConfig)) {
426
+ payload.modelConfig = normalizeModelConfigForModels(modelConfig, models);
395
427
  }
396
428
 
397
429
  updateProvider.mutate({ provider: providerName, data: payload });
@@ -596,7 +628,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
596
628
 
597
629
  <ProviderModelsSection
598
630
  models={models}
599
- modelThinking={modelThinking}
631
+ modelConfig={modelConfig}
600
632
  modelDraft={modelDraft}
601
633
  showModelInput={showModelInput}
602
634
  onModelDraftChange={setModelDraft}
@@ -604,7 +636,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
604
636
  onAddModel={handleAddModel}
605
637
  onRemoveModel={(modelName) => {
606
638
  setModels((prev) => prev.filter((name) => name !== modelName));
607
- setModelThinking((prev) => {
639
+ setModelConfig((prev) => {
608
640
  const next = { ...prev };
609
641
  delete next[modelName];
610
642
  return next;
@@ -612,6 +644,7 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
612
644
  }}
613
645
  onToggleModelThinkingLevel={toggleModelThinkingLevel}
614
646
  onSetModelThinkingDefault={setModelThinkingDefault}
647
+ onSetModelVision={setModelVision}
615
648
  thinkingLevels={THINKING_LEVELS}
616
649
  formatThinkingLevelLabel={formatThinkingLevelLabel}
617
650
  />
@@ -3,15 +3,19 @@ import { Input } from '@/shared/components/ui/input';
3
3
  import { Label } from '@/shared/components/ui/label';
4
4
  import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
5
5
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
6
+ import { Switch } from '@/shared/components/ui/switch';
6
7
  import { t } from '@/shared/lib/i18n';
7
8
  import type { ThinkingLevel } from '@/shared/lib/api';
8
9
  import { Plus, Settings2, X } from 'lucide-react';
9
10
 
10
- type ModelThinkingConfig = Record<string, { supported: ThinkingLevel[]; default?: ThinkingLevel | null }>;
11
+ type ModelConfig = Record<string, {
12
+ thinking?: { supported: ThinkingLevel[]; default?: ThinkingLevel | null };
13
+ vision?: boolean;
14
+ }>;
11
15
 
12
16
  type ProviderModelsSectionProps = {
13
17
  models: string[];
14
- modelThinking: ModelThinkingConfig;
18
+ modelConfig: ModelConfig;
15
19
  modelDraft: string;
16
20
  showModelInput: boolean;
17
21
  onModelDraftChange: (value: string) => void;
@@ -20,6 +24,7 @@ type ProviderModelsSectionProps = {
20
24
  onRemoveModel: (modelName: string) => void;
21
25
  onToggleModelThinkingLevel: (modelName: string, level: ThinkingLevel) => void;
22
26
  onSetModelThinkingDefault: (modelName: string, level: ThinkingLevel | null) => void;
27
+ onSetModelVision: (modelName: string, vision: boolean) => void;
23
28
  thinkingLevels: ThinkingLevel[];
24
29
  formatThinkingLevelLabel: (level: ThinkingLevel) => string;
25
30
  };
@@ -27,7 +32,7 @@ type ProviderModelsSectionProps = {
27
32
  export function ProviderModelsSection(props: ProviderModelsSectionProps) {
28
33
  const {
29
34
  models,
30
- modelThinking,
35
+ modelConfig,
31
36
  modelDraft,
32
37
  showModelInput,
33
38
  onModelDraftChange,
@@ -36,6 +41,7 @@ export function ProviderModelsSection(props: ProviderModelsSectionProps) {
36
41
  onRemoveModel,
37
42
  onToggleModelThinkingLevel,
38
43
  onSetModelThinkingDefault,
44
+ onSetModelVision,
39
45
  thinkingLevels,
40
46
  formatThinkingLevelLabel
41
47
  } = props;
@@ -108,9 +114,11 @@ export function ProviderModelsSection(props: ProviderModelsSectionProps) {
108
114
  ) : (
109
115
  <div className="flex flex-wrap gap-2">
110
116
  {models.map((modelName) => {
111
- const thinkingEntry = modelThinking[modelName];
117
+ const modelEntry = modelConfig[modelName];
118
+ const thinkingEntry = modelEntry?.thinking;
112
119
  const supportedLevels = thinkingEntry?.supported ?? [];
113
120
  const defaultThinkingLevel = thinkingEntry?.default ?? null;
121
+ const visionEnabled = modelEntry?.vision === true;
114
122
 
115
123
  return (
116
124
  <div
@@ -130,6 +138,13 @@ export function ProviderModelsSection(props: ProviderModelsSectionProps) {
130
138
  </button>
131
139
  </PopoverTrigger>
132
140
  <PopoverContent className="w-80 space-y-3">
141
+ <div className="flex items-center justify-between gap-3 rounded-lg border border-gray-100 bg-gray-50 px-3 py-2">
142
+ <div className="min-w-0">
143
+ <p className="text-xs font-semibold text-gray-800">{t('providerModelVisionTitle')}</p>
144
+ <p className="text-xs text-gray-500">{t('providerModelVisionHint')}</p>
145
+ </div>
146
+ <Switch checked={visionEnabled} onCheckedChange={(checked) => onSetModelVision(modelName, checked)} />
147
+ </div>
133
148
  <div className="space-y-1">
134
149
  <p className="text-xs font-semibold text-gray-800">{t('providerModelThinkingTitle')}</p>
135
150
  <p className="text-xs text-gray-500">{t('providerModelThinkingHint')}</p>
@@ -213,6 +213,7 @@ describe('ModelConfig', () => {
213
213
  prefix: 'openai',
214
214
  aliases: ['openai'],
215
215
  models: ['gpt-5.2'],
216
+ modelConfig: {},
216
217
  modelThinking: {},
217
218
  configured: true
218
219
  },
@@ -222,6 +223,7 @@ describe('ModelConfig', () => {
222
223
  prefix: 'customhub',
223
224
  aliases: ['customhub'],
224
225
  models: [],
226
+ modelConfig: {},
225
227
  modelThinking: {},
226
228
  configured: true
227
229
  }
@@ -3,7 +3,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
3
3
  import { cn } from '@/shared/lib/utils';
4
4
 
5
5
  const buttonVariants = cva(
6
- 'inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium ring-offset-background transition-all duration-fast focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
6
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium ring-offset-background transition-colors duration-fast focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
7
7
  {
8
8
  variants: {
9
9
  variant: {
@@ -4,6 +4,12 @@ export type ChannelAuthStartRequest = {
4
4
  domain?: string;
5
5
  };
6
6
 
7
+ export type ChannelAuthConnectRequest = {
8
+ accountId?: string;
9
+ domain?: string;
10
+ fields?: Record<string, unknown>;
11
+ };
12
+
7
13
  export type ChannelAuthStartResult = {
8
14
  channel: string;
9
15
  kind: "qr_code";
@@ -27,3 +33,5 @@ export type ChannelAuthPollResult = {
27
33
  accountId?: string | null;
28
34
  notes?: string[];
29
35
  };
36
+
37
+ export type ChannelAuthConnectResult = ChannelAuthPollResult;
@@ -35,10 +35,10 @@ export type BootstrapStatusView = {
35
35
  completedAt?: string;
36
36
  error?: string;
37
37
  };
38
- pluginHydration: {
38
+ extensionLoading: {
39
39
  state: BootstrapStageState;
40
- loadedPluginCount: number;
41
- totalPluginCount: number;
40
+ loadedExtensionCount: number;
41
+ totalExtensionCount: number;
42
42
  startedAt?: string;
43
43
  completedAt?: string;
44
44
  error?: string;
@@ -66,7 +66,10 @@ export type ProviderConfigView = {
66
66
  extraHeaders?: Record<string, string> | null;
67
67
  wireApi?: "auto" | "chat" | "responses" | null;
68
68
  models?: string[];
69
- modelThinking?: Record<string, { supported: ThinkingLevel[]; default?: ThinkingLevel | null }>;
69
+ modelConfig?: Record<string, {
70
+ thinking?: { supported: ThinkingLevel[]; default?: ThinkingLevel | null };
71
+ vision?: boolean;
72
+ }>;
70
73
  };
71
74
 
72
75
  export type ProviderConfigUpdate = {
@@ -77,7 +80,10 @@ export type ProviderConfigUpdate = {
77
80
  extraHeaders?: Record<string, string> | null;
78
81
  wireApi?: "auto" | "chat" | "responses" | null;
79
82
  models?: string[] | null;
80
- modelThinking?: Record<string, { supported?: ThinkingLevel[]; default?: ThinkingLevel | null }> | null;
83
+ modelConfig?: Record<string, {
84
+ thinking?: { supported?: ThinkingLevel[]; default?: ThinkingLevel | null };
85
+ vision?: boolean;
86
+ }> | null;
81
87
  };
82
88
 
83
89
  export type ProviderConnectionTestRequest = ProviderConfigUpdate & {
@@ -547,6 +553,10 @@ export type ProviderSpecView = {
547
553
  supportsCliImport?: boolean;
548
554
  };
549
555
  defaultModels?: string[];
556
+ modelConfig?: Record<string, {
557
+ thinking?: { supported: ThinkingLevel[]; default?: ThinkingLevel | null };
558
+ vision?: boolean;
559
+ }>;
550
560
  supportsWireApi?: boolean;
551
561
  wireApiOptions?: Array<"auto" | "chat" | "responses">;
552
562
  defaultWireApi?: "auto" | "chat" | "responses";
@@ -666,14 +676,13 @@ export type WsEvent =
666
676
  | { type: 'connection.close'; payload?: Record<string, unknown> }
667
677
  | { type: 'connection.error'; payload?: { message?: string } };
668
678
 
669
- export type MarketplaceItemType = 'plugin' | 'skill' | 'mcp';
679
+ export type MarketplaceItemType = 'skill' | 'mcp';
670
680
 
671
681
  export type MarketplaceSort = 'relevance' | 'updated';
672
682
 
673
- export type MarketplacePluginInstallKind = 'npm';
674
683
  export type MarketplaceSkillInstallKind = 'builtin' | 'marketplace';
675
684
  export type MarketplaceMcpInstallKind = 'template';
676
- export type MarketplaceInstallKind = MarketplacePluginInstallKind | MarketplaceSkillInstallKind | MarketplaceMcpInstallKind;
685
+ export type MarketplaceInstallKind = MarketplaceSkillInstallKind | MarketplaceMcpInstallKind;
677
686
 
678
687
  export type MarketplaceInstallSpec = {
679
688
  kind: MarketplaceInstallKind;
@@ -733,18 +742,6 @@ export type MarketplaceSkillContentView = {
733
742
  sourceUrl?: string;
734
743
  };
735
744
 
736
- export type MarketplacePluginContentView = {
737
- type: 'plugin';
738
- slug: string;
739
- name: string;
740
- install: MarketplaceInstallSpec;
741
- source: 'npm' | 'repo' | 'remote';
742
- raw?: string;
743
- bodyRaw?: string;
744
- metadataRaw?: string;
745
- sourceUrl?: string;
746
- };
747
-
748
745
  export type MarketplaceMcpContentView = {
749
746
  type: 'mcp';
750
747
  slug: string;
@@ -1,7 +1,9 @@
1
- import { nextclawClient } from '../managers/client.manager';
1
+ import { nextclawClient } from '@/shared/lib/api/managers/client.manager';
2
2
  import type {
3
3
  ChannelAuthPollRequest,
4
4
  ChannelAuthPollResult,
5
+ ChannelAuthConnectRequest,
6
+ ChannelAuthConnectResult,
5
7
  ChannelAuthStartRequest,
6
8
  ChannelAuthStartResult
7
9
  } from '@/shared/lib/api/channel-auth.types';
@@ -19,3 +21,10 @@ export async function pollChannelAuth(
19
21
  ): Promise<ChannelAuthPollResult> {
20
22
  return await nextclawClient.channelAuth.poll(channel, data);
21
23
  }
24
+
25
+ export async function connectChannelAuth(
26
+ channel: string,
27
+ data: ChannelAuthConnectRequest
28
+ ): Promise<ChannelAuthConnectResult> {
29
+ return await nextclawClient.channelAuth.connect(channel, data);
30
+ }
@@ -5,7 +5,6 @@ import type {
5
5
  MarketplaceManageRequest,
6
6
  MarketplaceManageResult,
7
7
  MarketplaceInstalledView,
8
- MarketplacePluginContentView,
9
8
  MarketplaceItemType,
10
9
  MarketplaceSkillContentView,
11
10
  MarketplaceItemView,
@@ -37,10 +36,6 @@ export async function fetchMarketplaceSkillContent(slug: string): Promise<Market
37
36
  return await nextclawClient.marketplace.fetchSkillContent(slug);
38
37
  }
39
38
 
40
- export async function fetchMarketplacePluginContent(slug: string): Promise<MarketplacePluginContentView> {
41
- return await nextclawClient.marketplace.fetchPluginContent(slug);
42
- }
43
-
44
39
  export async function fetchMarketplaceRecommendations(
45
40
  type: MarketplaceItemType,
46
41
  params: {