@vibe-forge/client 0.7.4 → 0.8.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 (141) hide show
  1. package/cli.cjs +16 -17
  2. package/dist/assets/{arc-DXs6SvQX.js → arc-BjI8Mzf5.js} +1 -1
  3. package/dist/assets/{blockDiagram-c4efeb88-h-xVkbzT.js → blockDiagram-c4efeb88-By4JL1RU.js} +1 -1
  4. package/dist/assets/{c4Diagram-c83219d4-DEumwLCr.js → c4Diagram-c83219d4-frpxdNO6.js} +1 -1
  5. package/dist/assets/channel-Da5T54-_.js +1 -0
  6. package/dist/assets/{classDiagram-beda092f-Dh_6VL8e.js → classDiagram-beda092f-sGBIwOiO.js} +1 -1
  7. package/dist/assets/{classDiagram-v2-2358418a-D9hG_V5y.js → classDiagram-v2-2358418a-JfASkQqT.js} +1 -1
  8. package/dist/assets/clone-BfjbcwWs.js +1 -0
  9. package/dist/assets/{createText-1719965b-DGO5tdKk.js → createText-1719965b-C6SwHZ-r.js} +1 -1
  10. package/dist/assets/{edges-96097737-63FzeZDk.js → edges-96097737-z-Dp4FKF.js} +1 -1
  11. package/dist/assets/{erDiagram-0228fc6a-jN2RzBTN.js → erDiagram-0228fc6a-5fIyCTtw.js} +1 -1
  12. package/dist/assets/{flowDb-c6c81e3f-CvND0Kz-.js → flowDb-c6c81e3f-DuDOLffh.js} +1 -1
  13. package/dist/assets/{flowDiagram-50d868cf-jtMtLi5z.js → flowDiagram-50d868cf-CqfJoZYm.js} +1 -1
  14. package/dist/assets/flowDiagram-v2-4f6560a1-B25RT9lb.js +1 -0
  15. package/dist/assets/{flowchart-elk-definition-6af322e1-Dic1wweO.js → flowchart-elk-definition-6af322e1-wpZusdN_.js} +1 -1
  16. package/dist/assets/{ganttDiagram-a2739b55-BLbYj7ru.js → ganttDiagram-a2739b55-aw70jAEI.js} +1 -1
  17. package/dist/assets/{gitGraphDiagram-82fe8481-Dm4ee53U.js → gitGraphDiagram-82fe8481-DhJVtfJF.js} +1 -1
  18. package/dist/assets/{graph-BnzAin3i.js → graph-Dp5XlF1F.js} +1 -1
  19. package/dist/assets/{index-5325376f-gU7GGRnq.js → index-5325376f-C7cRw1io.js} +1 -1
  20. package/dist/assets/{index-BRIfON-w.css → index-DHL1Qu5o.css} +1 -1
  21. package/dist/assets/index-DqioMim6.js +557 -0
  22. package/dist/assets/{infoDiagram-8eee0895-BI_1UH70.js → infoDiagram-8eee0895-B9VmKQm_.js} +1 -1
  23. package/dist/assets/{journeyDiagram-c64418c1-Xc6td0Nk.js → journeyDiagram-c64418c1-BTKwOAU-.js} +1 -1
  24. package/dist/assets/{layout-PHWoi3a3.js → layout-XtAsDaFY.js} +1 -1
  25. package/dist/assets/{line-BJPgSD92.js → line-1nd8Xc89.js} +1 -1
  26. package/dist/assets/{linear-DYKGy-mG.js → linear-BBztVBp6.js} +1 -1
  27. package/dist/assets/{mermaid.core-H3QJi-7A.js → mermaid.core-DaqQ11eY.js} +4 -4
  28. package/dist/assets/{mindmap-definition-8da855dc-UC--JAZa.js → mindmap-definition-8da855dc-DYdtyQbX.js} +1 -1
  29. package/dist/assets/{pieDiagram-a8764435-BTI_-cYX.js → pieDiagram-a8764435-CO9FnqSm.js} +1 -1
  30. package/dist/assets/{quadrantDiagram-1e28029f-C4Gf_SaX.js → quadrantDiagram-1e28029f-Cs-iTCZ-.js} +1 -1
  31. package/dist/assets/{requirementDiagram-08caed73-BKwfGAsO.js → requirementDiagram-08caed73-Diwrdq_y.js} +1 -1
  32. package/dist/assets/{sankeyDiagram-a04cb91d-DTp2p2pD.js → sankeyDiagram-a04cb91d-DjxNZwMs.js} +1 -1
  33. package/dist/assets/{sequenceDiagram-c5b8d532-CLuNEegU.js → sequenceDiagram-c5b8d532-CWawhoyM.js} +1 -1
  34. package/dist/assets/{stateDiagram-1ecb1508-BUofUUM6.js → stateDiagram-1ecb1508-Bow7IRrW.js} +1 -1
  35. package/dist/assets/{stateDiagram-v2-c2b004d7-BATuZH_y.js → stateDiagram-v2-c2b004d7-BJqu9_Fj.js} +1 -1
  36. package/dist/assets/{styles-b4e223ce-CVO41uVV.js → styles-b4e223ce-F2FDTYdm.js} +1 -1
  37. package/dist/assets/{styles-ca3715f6-fFE_-gsH.js → styles-ca3715f6-DJITgKSs.js} +1 -1
  38. package/dist/assets/{styles-d45a18b0-BeG4Dd2L.js → styles-d45a18b0-DMSpafXP.js} +1 -1
  39. package/dist/assets/{svgDrawCommon-b86b1483-D6PZVIuy.js → svgDrawCommon-b86b1483-3_yd3bB_.js} +1 -1
  40. package/dist/assets/{timeline-definition-faaaa080-CTFMc2GO.js → timeline-definition-faaaa080-CV5umgp5.js} +1 -1
  41. package/dist/assets/{xychartDiagram-f5964ef8-wWcw3yKn.js → xychartDiagram-f5964ef8-DhVTgtev.js} +1 -1
  42. package/dist/index.html +2 -2
  43. package/package.json +10 -8
  44. package/src/App.tsx +1 -1
  45. package/src/api/base.ts +7 -7
  46. package/src/api/benchmark.ts +7 -3
  47. package/src/api/config.ts +2 -1
  48. package/src/api.ts +1 -1
  49. package/src/components/ArchiveView.tsx +1 -1
  50. package/src/components/ConfigView.tsx +18 -6
  51. package/src/components/MarkdownContent.tsx +1 -1
  52. package/src/components/Sidebar.tsx +2 -2
  53. package/src/components/automation-view/RuleFormPanel.tsx +7 -5
  54. package/src/components/automation-view/TaskList.tsx +8 -5
  55. package/src/components/automation-view/TriggerList.tsx +25 -15
  56. package/src/components/automation-view/types.ts +1 -1
  57. package/src/components/benchmark-view/BenchmarkCasePanel.tsx +94 -94
  58. package/src/components/benchmark-view/BenchmarkSidebar.scss +8 -6
  59. package/src/components/benchmark-view/BenchmarkSidebar.tsx +43 -30
  60. package/src/components/benchmark-view/index.tsx +4 -2
  61. package/src/components/benchmark-view/types.ts +3 -2
  62. package/src/components/benchmark-view/utils.ts +1 -2
  63. package/src/components/chat/ChatHeader.tsx +1 -1
  64. package/src/components/chat/ChatHistoryView.tsx +3 -2
  65. package/src/components/chat/CurrentTodoList.tsx +2 -3
  66. package/src/components/chat/messages/MessageItem.tsx +15 -14
  67. package/src/components/chat/messages/message-utils.ts +1 -1
  68. package/src/components/chat/sender/Sender.scss +8 -3
  69. package/src/components/chat/sender/Sender.tsx +71 -22
  70. package/src/components/chat/session-timeline-panel/git-graph.ts +8 -1
  71. package/src/components/chat/session-timeline-panel/index.scss +2 -2
  72. package/src/components/chat/tools/DefaultTool.tsx +3 -3
  73. package/src/components/chat/tools/adapter-claude/BashTool.tsx +2 -2
  74. package/src/components/chat/tools/adapter-claude/GlobTool.tsx +2 -2
  75. package/src/components/chat/tools/adapter-claude/GrepTool.tsx +2 -2
  76. package/src/components/chat/tools/adapter-claude/LSTool.tsx +4 -4
  77. package/src/components/chat/tools/adapter-claude/ReadTool.scss +1 -2
  78. package/src/components/chat/tools/adapter-claude/ReadTool.tsx +3 -3
  79. package/src/components/chat/tools/adapter-claude/TodoTool.tsx +1 -1
  80. package/src/components/chat/tools/adapter-claude/WriteTool.tsx +2 -2
  81. package/src/components/chat/tools/adapter-claude/components/FileList.scss +4 -2
  82. package/src/components/chat/tools/core/ToolCallBox.scss +34 -35
  83. package/src/components/chat/tools/core/ToolGroup.tsx +5 -5
  84. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +1 -1
  85. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +1 -1
  86. package/src/components/chat/tools/task/StartTasksTool.tsx +2 -2
  87. package/src/components/chat/tools/task/components/TaskRow.tsx +4 -4
  88. package/src/components/chat/tools/task/components/TaskToolCard.tsx +4 -4
  89. package/src/components/config/ConfigAboutSection.tsx +1 -1
  90. package/src/components/config/ConfigSectionForm.tsx +2 -1
  91. package/src/components/config/ConfigSectionPanel.tsx +1 -1
  92. package/src/components/config/ConfigShortcutInput.scss +1 -1
  93. package/src/components/config/ConfigSourceSwitch.tsx +1 -1
  94. package/src/components/config/configSchema.ts +16 -1
  95. package/src/components/config/index.tsx +1 -1
  96. package/src/components/config/record-editors/McpServersRecordEditor.tsx +125 -123
  97. package/src/components/config/record-editors/ModelServicesRecordEditor.tsx +138 -136
  98. package/src/components/config/record-editors/RecordJsonEditor.tsx +31 -29
  99. package/src/components/config/record-editors/index.tsx +1 -1
  100. package/src/components/knowledge-base/components/EmptyState.tsx +1 -1
  101. package/src/components/knowledge-base/components/EntitiesTab.tsx +2 -2
  102. package/src/components/knowledge-base/components/EntityItem.tsx +1 -1
  103. package/src/components/knowledge-base/components/EntityList.tsx +1 -1
  104. package/src/components/knowledge-base/components/FilterBar.tsx +2 -2
  105. package/src/components/knowledge-base/components/FlowsTab.tsx +1 -1
  106. package/src/components/knowledge-base/components/KnowledgeList.tsx +1 -1
  107. package/src/components/knowledge-base/components/MetaList.tsx +1 -1
  108. package/src/components/knowledge-base/components/RuleItem.tsx +1 -1
  109. package/src/components/knowledge-base/components/RuleList.tsx +1 -1
  110. package/src/components/knowledge-base/components/RulesTab.tsx +3 -3
  111. package/src/components/knowledge-base/components/SectionHeader.tsx +1 -1
  112. package/src/components/knowledge-base/components/SkillsTab.tsx +3 -3
  113. package/src/components/knowledge-base/components/SpecItem.tsx +1 -1
  114. package/src/components/knowledge-base/components/SpecList.tsx +1 -1
  115. package/src/components/knowledge-base/components/TabContent.tsx +1 -1
  116. package/src/components/knowledge-base/components/TabLabel.tsx +1 -1
  117. package/src/components/sidebar/SessionItem.scss +0 -1
  118. package/src/components/sidebar/SessionItem.tsx +1 -1
  119. package/src/hooks/chat/model-selector.ts +115 -121
  120. package/src/hooks/chat/use-chat-adapter.ts +3 -3
  121. package/src/hooks/chat/use-chat-interaction.ts +1 -1
  122. package/src/hooks/chat/use-chat-model-adapter-selection.tsx +549 -0
  123. package/src/hooks/chat/use-chat-models.tsx +7 -2
  124. package/src/hooks/chat/use-chat-permission-mode.ts +5 -1
  125. package/src/hooks/chat/use-chat-session-messages.ts +2 -2
  126. package/src/hooks/chat/use-chat-session.ts +14 -12
  127. package/src/hooks/chat/use-chat-view.ts +1 -1
  128. package/src/hooks/use-app-preferences.ts +14 -4
  129. package/src/hooks/use-session-subscription.ts +17 -6
  130. package/src/hooks/useQueryParams.ts +8 -6
  131. package/src/resources/adapters.ts +8 -2
  132. package/src/resources/locales/en.json +14 -1
  133. package/src/resources/locales/zh.json +14 -1
  134. package/src/routes/ChatRoute.scss +5 -1
  135. package/src/runtime-config.ts +17 -13
  136. package/src/utils/shortcutUtils.ts +1 -1
  137. package/vite.config.ts +5 -0
  138. package/dist/assets/channel-Hxo8SEEx.js +0 -1
  139. package/dist/assets/clone-Dd_kUYh5.js +0 -1
  140. package/dist/assets/flowDiagram-v2-4f6560a1-CmztIxNZ.js +0 -1
  141. package/dist/assets/index-Cw-fkktx.js +0 -557
@@ -0,0 +1,549 @@
1
+ import React, { createElement, useCallback, useEffect, useMemo, useState } from 'react'
2
+ import type { ReactNode } from 'react'
3
+ import { useTranslation } from 'react-i18next'
4
+ import useSWR from 'swr'
5
+
6
+ import { getConfig } from '#~/api.js'
7
+ import { getAdapterDisplay } from '#~/resources/adapters.js'
8
+ import type {
9
+ AdapterBuiltinModel,
10
+ ConfigResponse,
11
+ ModelMetadataConfig,
12
+ ModelServiceConfig,
13
+ RecommendedModelConfig
14
+ } from '@vibe-forge/types'
15
+ import {
16
+ buildServiceModelSelector,
17
+ listServiceModels,
18
+ normalizeNonEmptyString,
19
+ resolveAdapterForChatModelSelection,
20
+ resolveAdapterModelCompatibility,
21
+ resolveChatAdapterSelection,
22
+ resolveChatModelSelection,
23
+ resolveDefaultChatModelSelection,
24
+ resolveModelForChatAdapterSelection,
25
+ resolveServiceModelSelector
26
+ } from './model-selector'
27
+
28
+ export interface ModelSelectOption {
29
+ value: string
30
+ label: React.ReactNode
31
+ searchText: string
32
+ displayLabel: string
33
+ }
34
+
35
+ export interface ModelSelectGroup {
36
+ label: React.ReactNode
37
+ options: ModelSelectOption[]
38
+ }
39
+
40
+ type SelectionDriver = 'adapter' | 'model'
41
+
42
+ const ADAPTER_STORAGE_KEY = 'vf_chat_adapter'
43
+ const MODEL_STORAGE_KEY = 'vf_chat_selected_model'
44
+ const DRIVER_STORAGE_KEY = 'vf_chat_selection_driver'
45
+
46
+ const readStorageValue = (key: string) => {
47
+ try {
48
+ const raw = localStorage.getItem(key)
49
+ return raw == null || raw.trim() === '' ? undefined : raw
50
+ } catch {
51
+ return undefined
52
+ }
53
+ }
54
+
55
+ const readSelectionDriver = (): SelectionDriver => {
56
+ const raw = readStorageValue(DRIVER_STORAGE_KEY)
57
+ return raw === 'model' ? 'model' : 'adapter'
58
+ }
59
+
60
+ const buildBuiltinModelValues = (models: AdapterBuiltinModel[] | undefined) => (
61
+ Array.isArray(models) ? models.map(model => model.value) : []
62
+ )
63
+
64
+ export function useChatModelAdapterSelection({
65
+ adapterLocked = false
66
+ }: {
67
+ adapterLocked?: boolean
68
+ } = {}) {
69
+ const { t } = useTranslation()
70
+ const [selectedAdapter, setSelectedAdapter] = useState<string | undefined>(() => readStorageValue(ADAPTER_STORAGE_KEY))
71
+ const [selectedModel, setSelectedModel] = useState<string | undefined>(() => readStorageValue(MODEL_STORAGE_KEY))
72
+ const [selectionDriver, setSelectionDriver] = useState<SelectionDriver>(() => readSelectionDriver())
73
+ const { data: configRes } = useSWR<ConfigResponse>('/api/config', getConfig)
74
+
75
+ const mergedAdapters = useMemo(() => {
76
+ return (configRes?.sources?.merged?.adapters ?? {}) as Record<string, unknown>
77
+ }, [configRes?.sources?.merged?.adapters])
78
+
79
+ const mergedModels = useMemo(() => {
80
+ return (configRes?.sources?.merged?.models ?? {}) as Record<string, ModelMetadataConfig>
81
+ }, [configRes?.sources?.merged?.models])
82
+
83
+ const mergedModelServices = useMemo(() => {
84
+ return (configRes?.sources?.merged?.modelServices ?? {}) as Record<string, ModelServiceConfig>
85
+ }, [configRes?.sources?.merged?.modelServices])
86
+
87
+ const recommendedModels = useMemo(() => {
88
+ const raw = configRes?.sources?.merged?.general?.recommendedModels
89
+ if (!Array.isArray(raw)) return []
90
+ return raw.filter((item): item is RecommendedModelConfig => (
91
+ item != null && typeof item === 'object' && typeof item.model === 'string' && item.model.trim() !== ''
92
+ ))
93
+ }, [configRes?.sources?.merged?.general?.recommendedModels])
94
+
95
+ const adapterBuiltinModels = useMemo(() => {
96
+ return (configRes?.sources?.merged?.adapterBuiltinModels ?? {}) as Record<string, AdapterBuiltinModel[]>
97
+ }, [configRes?.sources?.merged?.adapterBuiltinModels])
98
+
99
+ const defaultAdapter = normalizeNonEmptyString(configRes?.sources?.merged?.general?.defaultAdapter)
100
+ const defaultModelService = normalizeNonEmptyString(configRes?.sources?.merged?.general?.defaultModelService)
101
+ const defaultModel = normalizeNonEmptyString(configRes?.sources?.merged?.general?.defaultModel)
102
+
103
+ const availableAdapters = useMemo(() => Object.keys(mergedAdapters), [mergedAdapters])
104
+ const availableServiceModels = useMemo(() => listServiceModels(mergedModelServices), [mergedModelServices])
105
+ const allBuiltinModelValues = useMemo(() => (
106
+ Object.values(adapterBuiltinModels).flatMap(models => buildBuiltinModelValues(models))
107
+ ), [adapterBuiltinModels])
108
+ const activeBuiltinModels = useMemo(() => {
109
+ if (selectedAdapter && adapterBuiltinModels[selectedAdapter]) {
110
+ return { [selectedAdapter]: adapterBuiltinModels[selectedAdapter] }
111
+ }
112
+ return adapterBuiltinModels
113
+ }, [adapterBuiltinModels, selectedAdapter])
114
+ const activeBuiltinModelValues = useMemo(() => (
115
+ Object.values(activeBuiltinModels).flatMap(models => buildBuiltinModelValues(models))
116
+ ), [activeBuiltinModels])
117
+ const hasAvailableModels = availableServiceModels.length > 0 || activeBuiltinModelValues.length > 0
118
+
119
+ const resolveAdapterValue = useCallback((value?: string) => {
120
+ return resolveChatAdapterSelection({
121
+ value,
122
+ availableAdapters,
123
+ defaultAdapter
124
+ })
125
+ }, [availableAdapters, defaultAdapter])
126
+
127
+ const resolveSelectableModel = useCallback((value?: string, builtinModels?: Iterable<string>, preserveUnknown = false) => {
128
+ return resolveChatModelSelection({
129
+ value,
130
+ builtinModels,
131
+ serviceModels: availableServiceModels,
132
+ defaultModelService,
133
+ preserveUnknown
134
+ })
135
+ }, [availableServiceModels, defaultModelService])
136
+
137
+ const resolveModelForAdapter = useCallback((adapter?: string) => {
138
+ const builtinModels = buildBuiltinModelValues(
139
+ adapter != null ? adapterBuiltinModels[adapter] : undefined
140
+ )
141
+ const resolvedModel = resolveModelForChatAdapterSelection({
142
+ adapter,
143
+ adapters: mergedAdapters,
144
+ defaultModel,
145
+ defaultModelService,
146
+ builtinModels,
147
+ fallbackBuiltinModels: allBuiltinModelValues,
148
+ serviceModels: availableServiceModels
149
+ })
150
+ if (!adapter || !resolvedModel) return resolvedModel
151
+
152
+ const compatibility = resolveAdapterModelCompatibility({
153
+ adapter,
154
+ model: resolvedModel,
155
+ adapterConfig: mergedAdapters[adapter],
156
+ builtinModels,
157
+ serviceModels: availableServiceModels,
158
+ preferredServiceKey: defaultModelService,
159
+ preserveUnknownDefaultModel: false
160
+ })
161
+ return compatibility.model ?? resolvedModel
162
+ }, [
163
+ adapterBuiltinModels,
164
+ allBuiltinModelValues,
165
+ availableServiceModels,
166
+ defaultModel,
167
+ defaultModelService,
168
+ mergedAdapters
169
+ ])
170
+
171
+ const resolveCompatibleModelForAdapter = useCallback((adapter: string | undefined, model: string | undefined) => {
172
+ if (!adapter || !model) return model
173
+
174
+ const compatibility = resolveAdapterModelCompatibility({
175
+ adapter,
176
+ model,
177
+ adapterConfig: mergedAdapters[adapter],
178
+ builtinModels: buildBuiltinModelValues(adapterBuiltinModels[adapter]),
179
+ serviceModels: availableServiceModels,
180
+ preferredServiceKey: defaultModelService,
181
+ preserveUnknownDefaultModel: false
182
+ })
183
+
184
+ return compatibility.model ?? model
185
+ }, [
186
+ adapterBuiltinModels,
187
+ availableServiceModels,
188
+ defaultModelService,
189
+ mergedAdapters
190
+ ])
191
+
192
+ const resolveAdapterForModel = useCallback((model?: string) => {
193
+ return resolveAdapterForChatModelSelection({
194
+ model,
195
+ availableAdapters,
196
+ defaultAdapter,
197
+ adapterBuiltinModels,
198
+ modelMetadata: mergedModels
199
+ })
200
+ }, [adapterBuiltinModels, availableAdapters, defaultAdapter, mergedModels])
201
+
202
+ const resolvedDefaultModel = useMemo(() => {
203
+ return resolveDefaultChatModelSelection({
204
+ defaultModel,
205
+ defaultModelService,
206
+ builtinModels: allBuiltinModelValues,
207
+ serviceModels: availableServiceModels,
208
+ preserveUnknownDefaultModel: false
209
+ })
210
+ }, [allBuiltinModelValues, availableServiceModels, defaultModel, defaultModelService])
211
+
212
+ useEffect(() => {
213
+ if (adapterLocked) return
214
+
215
+ if (availableAdapters.length === 0) {
216
+ setSelectedAdapter(undefined)
217
+ if (!hasAvailableModels) setSelectedModel(undefined)
218
+ return
219
+ }
220
+
221
+ if (!hasAvailableModels) {
222
+ setSelectedModel(undefined)
223
+ setSelectedAdapter((prev) => resolveAdapterValue(prev))
224
+ return
225
+ }
226
+
227
+ if (selectionDriver === 'model') {
228
+ const nextModelCandidate = resolveSelectableModel(selectedModel, allBuiltinModelValues, false) ?? resolvedDefaultModel
229
+ const nextAdapter = resolveAdapterForModel(nextModelCandidate) ?? resolveAdapterValue(selectedAdapter)
230
+ const nextModel = resolveCompatibleModelForAdapter(nextAdapter, nextModelCandidate)
231
+ setSelectedModel((prev) => prev === nextModel ? prev : nextModel)
232
+ setSelectedAdapter((prev) => prev === nextAdapter ? prev : nextAdapter)
233
+ return
234
+ }
235
+
236
+ const nextAdapter = resolveAdapterValue(selectedAdapter)
237
+ const nextModel = resolveModelForAdapter(nextAdapter)
238
+ setSelectedAdapter((prev) => prev === nextAdapter ? prev : nextAdapter)
239
+ setSelectedModel((prev) => prev === nextModel ? prev : nextModel)
240
+ }, [
241
+ adapterLocked,
242
+ allBuiltinModelValues,
243
+ availableAdapters.length,
244
+ hasAvailableModels,
245
+ resolveAdapterForModel,
246
+ resolveCompatibleModelForAdapter,
247
+ resolveAdapterValue,
248
+ resolveModelForAdapter,
249
+ resolveSelectableModel,
250
+ resolvedDefaultModel,
251
+ selectedAdapter,
252
+ selectedModel,
253
+ selectionDriver
254
+ ])
255
+
256
+ useEffect(() => {
257
+ try {
258
+ if (selectedAdapter == null || selectedAdapter.trim() === '') {
259
+ localStorage.removeItem(ADAPTER_STORAGE_KEY)
260
+ } else {
261
+ localStorage.setItem(ADAPTER_STORAGE_KEY, selectedAdapter)
262
+ }
263
+ } catch {}
264
+ }, [selectedAdapter])
265
+
266
+ useEffect(() => {
267
+ try {
268
+ if (selectedModel == null || selectedModel.trim() === '') {
269
+ localStorage.removeItem(MODEL_STORAGE_KEY)
270
+ } else {
271
+ localStorage.setItem(MODEL_STORAGE_KEY, selectedModel)
272
+ }
273
+ } catch {}
274
+ }, [selectedModel])
275
+
276
+ useEffect(() => {
277
+ try {
278
+ localStorage.setItem(DRIVER_STORAGE_KEY, selectionDriver)
279
+ } catch {}
280
+ }, [selectionDriver])
281
+
282
+ const updateSelectedModel = useCallback((value?: string) => {
283
+ const builtinModels = adapterLocked
284
+ ? buildBuiltinModelValues(selectedAdapter != null ? adapterBuiltinModels[selectedAdapter] : undefined)
285
+ : allBuiltinModelValues
286
+ const nextModel = resolveSelectableModel(value, builtinModels, false)
287
+ if (!nextModel) return
288
+
289
+ setSelectionDriver('model')
290
+ const nextAdapter = adapterLocked
291
+ ? selectedAdapter
292
+ : (resolveAdapterForModel(nextModel) ?? resolveAdapterValue(selectedAdapter))
293
+ const resolvedNextModel = resolveCompatibleModelForAdapter(nextAdapter, nextModel)
294
+ setSelectedModel((prev) => prev === resolvedNextModel ? prev : resolvedNextModel)
295
+
296
+ if (adapterLocked) return
297
+
298
+ setSelectedAdapter((prev) => prev === nextAdapter ? prev : nextAdapter)
299
+ }, [
300
+ adapterBuiltinModels,
301
+ adapterLocked,
302
+ allBuiltinModelValues,
303
+ resolveCompatibleModelForAdapter,
304
+ resolveAdapterForModel,
305
+ resolveAdapterValue,
306
+ resolveSelectableModel,
307
+ selectedAdapter
308
+ ])
309
+
310
+ const updateSelectedAdapter = useCallback((value?: string) => {
311
+ const nextAdapter = resolveAdapterValue(value)
312
+ setSelectionDriver('adapter')
313
+ setSelectedAdapter((prev) => prev === nextAdapter ? prev : nextAdapter)
314
+
315
+ if (adapterLocked) return
316
+
317
+ const nextModel = resolveModelForAdapter(nextAdapter)
318
+ setSelectedModel((prev) => prev === nextModel ? prev : nextModel)
319
+ }, [adapterLocked, resolveAdapterValue, resolveModelForAdapter])
320
+
321
+ const applySessionSelection = useCallback((params: { model?: string; adapter?: string }) => {
322
+ const nextAdapter = normalizeNonEmptyString(params.adapter) ?? resolveAdapterValue(undefined)
323
+ const sessionBuiltinModels = buildBuiltinModelValues(
324
+ nextAdapter != null ? adapterBuiltinModels[nextAdapter] : undefined
325
+ )
326
+ const nextModel = resolveSelectableModel(params.model, sessionBuiltinModels, true) ??
327
+ resolveSelectableModel(params.model, allBuiltinModelValues, true) ??
328
+ normalizeNonEmptyString(params.model) ??
329
+ resolveModelForAdapter(nextAdapter)
330
+
331
+ setSelectedAdapter((prev) => prev === nextAdapter ? prev : nextAdapter)
332
+ setSelectedModel((prev) => prev === nextModel ? prev : nextModel)
333
+ }, [
334
+ adapterBuiltinModels,
335
+ allBuiltinModelValues,
336
+ resolveAdapterValue,
337
+ resolveModelForAdapter,
338
+ resolveSelectableModel
339
+ ])
340
+
341
+ const selectedModelWithService = useMemo(() => (
342
+ resolveSelectableModel(selectedModel, activeBuiltinModelValues, true) ?? selectedModel
343
+ ), [activeBuiltinModelValues, resolveSelectableModel, selectedModel])
344
+
345
+ const adapterOptions = useMemo<Array<{ value: string; label: ReactNode }>>(() => {
346
+ return availableAdapters.map((key) => {
347
+ const display = getAdapterDisplay(key)
348
+ return {
349
+ value: key,
350
+ label: createElement('span', { className: 'adapter-option' }, [
351
+ display.icon != null
352
+ ? createElement('img', {
353
+ key: 'icon',
354
+ className: 'adapter-option__icon',
355
+ src: display.icon,
356
+ alt: '',
357
+ 'aria-hidden': true
358
+ })
359
+ : null,
360
+ createElement('span', { key: 'text', className: 'adapter-option__text' }, display.title)
361
+ ])
362
+ }
363
+ })
364
+ }, [availableAdapters])
365
+
366
+ const modelServiceEntries = useMemo(() => Object.entries(mergedModelServices), [mergedModelServices])
367
+ const modelToService = useMemo(() => {
368
+ const map = new Map<string, { key: string; title: string }>()
369
+ for (const entry of availableServiceModels) {
370
+ const serviceValue = mergedModelServices[entry.serviceKey]
371
+ const serviceTitle = serviceValue?.title?.trim() !== '' ? serviceValue?.title ?? '' : entry.serviceKey
372
+ if (!map.has(entry.model)) {
373
+ map.set(entry.model, { key: entry.serviceKey, title: serviceTitle })
374
+ }
375
+ }
376
+ return map
377
+ }, [availableServiceModels, mergedModelServices])
378
+
379
+ const modelOptions = useMemo<ModelSelectGroup[]>(() => {
380
+ const buildOption = (params: {
381
+ value: string
382
+ title: string
383
+ description?: string
384
+ serviceKey?: string
385
+ serviceTitle?: string
386
+ }) => {
387
+ const description = params.description?.trim()
388
+ const label = (
389
+ <div className='model-option'>
390
+ <div className='model-option-title'>{params.title}</div>
391
+ {description && <div className='model-option-desc'>{description}</div>}
392
+ </div>
393
+ )
394
+ const searchText = [
395
+ params.title,
396
+ params.value,
397
+ params.serviceTitle,
398
+ params.serviceKey,
399
+ description
400
+ ]
401
+ .filter(Boolean)
402
+ .join(' ')
403
+ return {
404
+ value: params.value,
405
+ label,
406
+ searchText,
407
+ displayLabel: params.title
408
+ }
409
+ }
410
+
411
+ const resolveFirstAlias = (modelsAlias: Record<string, string[]> | undefined, model: string) => {
412
+ if (!modelsAlias) return undefined
413
+ for (const [alias, aliasModels] of Object.entries(modelsAlias)) {
414
+ if (!Array.isArray(aliasModels)) continue
415
+ if (aliasModels.includes(model)) return alias
416
+ }
417
+ return undefined
418
+ }
419
+
420
+ const serviceGroups = modelServiceEntries
421
+ .map(([serviceKey, serviceValue]) => {
422
+ const service = (serviceValue != null && typeof serviceValue === 'object')
423
+ ? serviceValue as ModelServiceConfig
424
+ : undefined
425
+ const serviceTitle = service?.title?.trim() !== '' ? service?.title ?? '' : serviceKey
426
+ const groupTitle = serviceTitle?.trim() !== '' ? serviceTitle : serviceKey
427
+ const serviceDescription = service?.description
428
+ const models = Array.isArray(service?.models)
429
+ ? service.models.filter((item: unknown): item is string => typeof item === 'string')
430
+ : []
431
+ if (models.length === 0) return null
432
+ const options = models.map((model: string) => {
433
+ const alias = resolveFirstAlias(service?.modelsAlias as Record<string, string[]> | undefined, model)
434
+ const title = alias ?? model
435
+ const description = alias ? model : serviceTitle
436
+ return buildOption({
437
+ value: buildServiceModelSelector(serviceKey, model),
438
+ title,
439
+ description,
440
+ serviceKey,
441
+ serviceTitle
442
+ })
443
+ })
444
+ return {
445
+ label: (
446
+ <div className='model-group-label'>
447
+ <div className='model-group-title'>{groupTitle}</div>
448
+ {serviceDescription && <div className='model-group-desc'>{serviceDescription}</div>}
449
+ </div>
450
+ ),
451
+ options
452
+ }
453
+ })
454
+ .filter((item): item is NonNullable<typeof item> => item != null)
455
+
456
+ const recommendedOptions = recommendedModels
457
+ .filter((item) => {
458
+ if (item.placement && item.placement !== 'modelSelector') return false
459
+ return resolveServiceModelSelector({
460
+ value: item.service ? buildServiceModelSelector(item.service, item.model) : item.model,
461
+ serviceModels: availableServiceModels,
462
+ preferredServiceKey: item.service ?? defaultModelService
463
+ }) != null
464
+ })
465
+ .map((item) => {
466
+ const serviceInfo = item.service ? mergedModelServices[item.service] : undefined
467
+ const serviceTitle = item.service
468
+ ? (serviceInfo?.title?.trim() !== '' ? serviceInfo?.title ?? '' : item.service)
469
+ : modelToService.get(item.model)?.title
470
+ const alias = item.service
471
+ ? resolveFirstAlias(serviceInfo?.modelsAlias as Record<string, string[]> | undefined, item.model)
472
+ : undefined
473
+ const title = item.title?.trim() !== '' ? item.title ?? '' : (alias ?? item.model)
474
+ const description = item.description?.trim() !== ''
475
+ ? item.description
476
+ : serviceTitle
477
+ const value = resolveServiceModelSelector({
478
+ value: item.service ? buildServiceModelSelector(item.service, item.model) : item.model,
479
+ serviceModels: availableServiceModels,
480
+ preferredServiceKey: item.service ?? defaultModelService
481
+ }) ?? item.model
482
+ return buildOption({
483
+ value,
484
+ title,
485
+ description,
486
+ serviceKey: item.service ?? modelToService.get(item.model)?.key,
487
+ serviceTitle
488
+ })
489
+ })
490
+
491
+ const groups = []
492
+ if (recommendedOptions.length > 0) {
493
+ const recommendedTitle = t('chat.modelGroupRecommended', { defaultValue: '推荐模型' })
494
+ groups.push({
495
+ label: (
496
+ <div className='model-group-label'>
497
+ <div className='model-group-title'>{recommendedTitle}</div>
498
+ </div>
499
+ ),
500
+ options: recommendedOptions
501
+ })
502
+ }
503
+
504
+ for (const [adapterKey, models] of Object.entries(activeBuiltinModels)) {
505
+ if (!Array.isArray(models) || models.length === 0) continue
506
+ const adapterTitle = t('chat.modelGroupBuiltin', {
507
+ adapter: adapterKey,
508
+ defaultValue: `${adapterKey} (Default)`
509
+ })
510
+ groups.push({
511
+ label: (
512
+ <div className='model-group-label'>
513
+ <div className='model-group-title'>{adapterTitle}</div>
514
+ </div>
515
+ ),
516
+ options: models.map(model =>
517
+ buildOption({
518
+ value: model.value,
519
+ title: model.title,
520
+ description: model.description
521
+ })
522
+ )
523
+ })
524
+ }
525
+
526
+ return [...groups, ...serviceGroups]
527
+ }, [
528
+ activeBuiltinModels,
529
+ availableServiceModels,
530
+ defaultModelService,
531
+ mergedModelServices,
532
+ modelServiceEntries,
533
+ modelToService,
534
+ recommendedModels,
535
+ t
536
+ ])
537
+
538
+ return {
539
+ adapterOptions,
540
+ applySessionSelection,
541
+ hasAvailableModels,
542
+ modelOptions,
543
+ selectedAdapter,
544
+ selectedModel,
545
+ selectedModelWithService,
546
+ setSelectedAdapter: updateSelectedAdapter,
547
+ setSelectedModel: updateSelectedModel
548
+ }
549
+ }
@@ -3,7 +3,12 @@ import { useTranslation } from 'react-i18next'
3
3
  import useSWR from 'swr'
4
4
 
5
5
  import { getConfig } from '#~/api.js'
6
- import type { AdapterBuiltinModel, ConfigResponse, ModelServiceConfig, RecommendedModelConfig } from '@vibe-forge/core'
6
+ import type {
7
+ AdapterBuiltinModel,
8
+ ConfigResponse,
9
+ ModelServiceConfig,
10
+ RecommendedModelConfig
11
+ } from '@vibe-forge/types'
7
12
  import {
8
13
  buildServiceModelSelector,
9
14
  listServiceModels,
@@ -204,7 +209,7 @@ export function useChatModels({
204
209
  const groupTitle = serviceTitle?.trim() !== '' ? serviceTitle : serviceKey
205
210
  const serviceDescription = service?.description
206
211
  const models = Array.isArray(service?.models)
207
- ? service.models.filter((item): item is string => typeof item === 'string')
212
+ ? service.models.filter((item: unknown): item is string => typeof item === 'string')
208
213
  : []
209
214
  if (models.length === 0) return null
210
215
  const options = models.map((model: string) => {
@@ -17,7 +17,11 @@ export function useChatPermissionMode() {
17
17
  const [permissionMode, setPermissionMode] = useState<PermissionMode>('default')
18
18
 
19
19
  const updatePermissionMode = (value?: string) => {
20
- setPermissionMode(isPermissionMode(value ?? '') ? value : 'default')
20
+ if (value != null && isPermissionMode(value)) {
21
+ setPermissionMode(value)
22
+ return
23
+ }
24
+ setPermissionMode('default')
21
25
  }
22
26
 
23
27
  useEffect(() => {
@@ -1,11 +1,11 @@
1
- import { App } from 'antd'
2
1
  import { useCallback, useEffect, useRef, useState } from 'react'
3
2
  import { useTranslation } from 'react-i18next'
4
3
  import { useSWRConfig } from 'swr'
5
4
 
6
5
  import { getSessionMessages } from '#~/api.js'
7
6
  import { connectionManager } from '#~/connectionManager.js'
8
- import type { AskUserQuestionParams, ChatMessage, Session, SessionInfo, WSEvent } from '@vibe-forge/core'
7
+ import type { SessionInfo } from '@vibe-forge/types'
8
+ import type { AskUserQuestionParams, ChatMessage, Session, WSEvent } from '@vibe-forge/core'
9
9
  import type { PermissionMode } from './use-chat-permission-mode'
10
10
 
11
11
  const applyMessageEvent = (currentMessages: ChatMessage[], data: WSEvent) => {
@@ -2,9 +2,8 @@ import { useEffect, useRef } from 'react'
2
2
  import { useTranslation } from 'react-i18next'
3
3
 
4
4
  import type { Session } from '@vibe-forge/core'
5
- import { useChatAdapter } from './use-chat-adapter'
6
5
  import { useChatInteraction } from './use-chat-interaction'
7
- import { useChatModels } from './use-chat-models'
6
+ import { useChatModelAdapterSelection } from './use-chat-model-adapter-selection'
8
7
  import { useChatPermissionMode } from './use-chat-permission-mode'
9
8
  import { useChatSessionMessages } from './use-chat-session-messages'
10
9
  import { useChatView } from './use-chat-view'
@@ -15,14 +14,19 @@ export function useChatSession({
15
14
  session?: Session
16
15
  }) {
17
16
  const { t } = useTranslation()
18
- const { selectedAdapter, setSelectedAdapter, adapterOptions } = useChatAdapter()
19
17
  const {
18
+ adapterOptions,
19
+ applySessionSelection,
20
+ selectedAdapter,
20
21
  selectedModel,
21
22
  selectedModelWithService,
22
23
  setSelectedModel,
24
+ setSelectedAdapter,
23
25
  modelOptions,
24
26
  hasAvailableModels
25
- } = useChatModels({ selectedAdapter })
27
+ } = useChatModelAdapterSelection({
28
+ adapterLocked: session?.id != null
29
+ })
26
30
  const { permissionMode, setPermissionMode, permissionModeOptions } = useChatPermissionMode()
27
31
  const { activeView, setActiveView } = useChatView()
28
32
  const { interactionRequest, setInteractionRequest, handleInteractionResponse } = useChatInteraction({
@@ -47,18 +51,17 @@ export function useChatSession({
47
51
  const previous = lastObservedSessionRef.current
48
52
  const sessionChanged = previous?.id !== session.id
49
53
 
50
- if (sessionChanged || previous?.model !== session.model) {
51
- setSelectedModel(session.model)
54
+ if (sessionChanged || previous?.model !== session.model || previous?.adapter !== session.adapter) {
55
+ applySessionSelection({
56
+ model: session.model,
57
+ adapter: session.adapter
58
+ })
52
59
  }
53
60
 
54
61
  if (sessionChanged || previous?.permissionMode !== session.permissionMode) {
55
62
  setPermissionMode(session.permissionMode)
56
63
  }
57
64
 
58
- if (sessionChanged || previous?.adapter !== session.adapter) {
59
- setSelectedAdapter(session.adapter)
60
- }
61
-
62
65
  lastObservedSessionRef.current = {
63
66
  id: session.id,
64
67
  model: session.model,
@@ -70,9 +73,8 @@ export function useChatSession({
70
73
  session?.id,
71
74
  session?.model,
72
75
  session?.permissionMode,
76
+ applySessionSelection,
73
77
  setPermissionMode,
74
- setSelectedAdapter,
75
- setSelectedModel
76
78
  ])
77
79
 
78
80
  return {
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect } from 'react'
2
2
 
3
- import { useQueryParams } from '#~/hooks/useQueryParams.js'
4
3
  import type { ChatHeaderView } from '#~/components/chat/ChatHeader.js'
4
+ import { useQueryParams } from '#~/hooks/useQueryParams.js'
5
5
 
6
6
  const normalizeView = (value: string): ChatHeaderView => {
7
7
  if (value === 'timeline' || value === 'settings' || value === 'history') {