@vibe-forge/client 0.4.0 → 0.6.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 (142) hide show
  1. package/AGENTS.md +37 -0
  2. package/cli.cjs +1 -0
  3. package/dist/assets/{arc-DgIxeTMg.js → arc-CMAHd5G3.js} +1 -1
  4. package/dist/assets/{blockDiagram-c4efeb88-CEAob3X9.js → blockDiagram-c4efeb88-DKww-VCP.js} +1 -1
  5. package/dist/assets/{c4Diagram-c83219d4-DwIxpDKd.js → c4Diagram-c83219d4-DKrjVHyY.js} +1 -1
  6. package/dist/assets/channel-Bi4g8rj9.js +1 -0
  7. package/dist/assets/{classDiagram-beda092f-Cz1q8u_0.js → classDiagram-beda092f-BXx5rdo3.js} +1 -1
  8. package/dist/assets/{classDiagram-v2-2358418a-CImgTuwd.js → classDiagram-v2-2358418a-CnR3WLsr.js} +1 -1
  9. package/dist/assets/clone-DPrpP2ky.js +1 -0
  10. package/dist/assets/{createText-1719965b-C1_HJcCc.js → createText-1719965b-CmOsl1W7.js} +1 -1
  11. package/dist/assets/{edges-96097737-BU8qStzd.js → edges-96097737-CQeQgpjD.js} +1 -1
  12. package/dist/assets/{erDiagram-0228fc6a-DNA1Fz2L.js → erDiagram-0228fc6a-ZUNB-ucF.js} +1 -1
  13. package/dist/assets/{flowDb-c6c81e3f-DjiCStMN.js → flowDb-c6c81e3f-DuuKeSLX.js} +1 -1
  14. package/dist/assets/{flowDiagram-50d868cf-CSDi0-RD.js → flowDiagram-50d868cf-Bc6n85yR.js} +1 -1
  15. package/dist/assets/flowDiagram-v2-4f6560a1-BZqaeqoh.js +1 -0
  16. package/dist/assets/{flowchart-elk-definition-6af322e1-DrhIMas7.js → flowchart-elk-definition-6af322e1-cAG5afW9.js} +1 -1
  17. package/dist/assets/{ganttDiagram-a2739b55-CTZnUP5z.js → ganttDiagram-a2739b55-Dp6xhY5I.js} +1 -1
  18. package/dist/assets/{gitGraphDiagram-82fe8481-COOW7jTi.js → gitGraphDiagram-82fe8481-MlIIRBdG.js} +1 -1
  19. package/dist/assets/{graph-CIkpD4Kx.js → graph-D7Es8jZ-.js} +1 -1
  20. package/dist/assets/{index-5325376f-aVVRRTIu.js → index-5325376f-DC18ottv.js} +1 -1
  21. package/dist/assets/index-D37AbgPQ.js +545 -0
  22. package/dist/assets/{index-D1giUI7r.css → index-fcJ9v94I.css} +1 -1
  23. package/dist/assets/{infoDiagram-8eee0895-DQpZ1LVD.js → infoDiagram-8eee0895-CXk21kFp.js} +1 -1
  24. package/dist/assets/{journeyDiagram-c64418c1-DoKguIuk.js → journeyDiagram-c64418c1-899BKBHL.js} +1 -1
  25. package/dist/assets/{layout-Tnmha8Nh.js → layout-DLaxdy48.js} +1 -1
  26. package/dist/assets/{line-BQR2SOyl.js → line-_lw5YbRM.js} +1 -1
  27. package/dist/assets/{linear-DlG0eemV.js → linear-D5iu84ui.js} +1 -1
  28. package/dist/assets/{mermaid.core-BnwYO0He.js → mermaid.core-C6sW3GFM.js} +4 -4
  29. package/dist/assets/{mindmap-definition-8da855dc-BllYwDID.js → mindmap-definition-8da855dc-BS9Xy9KN.js} +1 -1
  30. package/dist/assets/{pieDiagram-a8764435-DwCkhPVc.js → pieDiagram-a8764435-DZt9cEgs.js} +1 -1
  31. package/dist/assets/{quadrantDiagram-1e28029f-c40GKTU0.js → quadrantDiagram-1e28029f-BTIeHOgn.js} +1 -1
  32. package/dist/assets/{requirementDiagram-08caed73-DnQp2Tk6.js → requirementDiagram-08caed73-BHJAKD2g.js} +1 -1
  33. package/dist/assets/{sankeyDiagram-a04cb91d-CnJrs13b.js → sankeyDiagram-a04cb91d-DnAkVOK8.js} +1 -1
  34. package/dist/assets/{sequenceDiagram-c5b8d532-1YBwnpKu.js → sequenceDiagram-c5b8d532-92tE3oFv.js} +1 -1
  35. package/dist/assets/{stateDiagram-1ecb1508-BFBxQ6Fh.js → stateDiagram-1ecb1508-DG0ObiMg.js} +1 -1
  36. package/dist/assets/{stateDiagram-v2-c2b004d7-Dmechvv2.js → stateDiagram-v2-c2b004d7-BKoJx2ci.js} +1 -1
  37. package/dist/assets/{styles-b4e223ce-DWWfWX8O.js → styles-b4e223ce-Ba6G4ri9.js} +1 -1
  38. package/dist/assets/{styles-ca3715f6-CKKvZxaU.js → styles-ca3715f6-Bn6RIIVW.js} +1 -1
  39. package/dist/assets/{styles-d45a18b0-dKMOUh9p.js → styles-d45a18b0-_dELBUI6.js} +1 -1
  40. package/dist/assets/{svgDrawCommon-b86b1483-CBgjChPM.js → svgDrawCommon-b86b1483-CRK-ZoIs.js} +1 -1
  41. package/dist/assets/{timeline-definition-faaaa080-NCt-HHmb.js → timeline-definition-faaaa080-DvQ_RA_i.js} +1 -1
  42. package/dist/assets/{xychartDiagram-f5964ef8-BJhXS4dG.js → xychartDiagram-f5964ef8-CJxeDLfg.js} +1 -1
  43. package/dist/index.html +2 -2
  44. package/package.json +10 -7
  45. package/src/App.tsx +20 -168
  46. package/src/api/base.ts +116 -7
  47. package/src/api/sessions.ts +3 -1
  48. package/src/api.ts +3 -1
  49. package/src/components/ArchiveView.tsx +5 -5
  50. package/src/components/ConfigView.tsx +28 -28
  51. package/src/components/{AutomationView → automation-view}/index.tsx +10 -9
  52. package/src/components/{BenchmarkView → benchmark-view}/index.tsx +5 -4
  53. package/src/components/chat/ChatHeader.tsx +6 -6
  54. package/src/components/chat/ChatHistoryView.tsx +74 -16
  55. package/src/components/chat/ChatTimelineView.tsx +3 -3
  56. package/src/components/chat/CurrentTodoList.scss +56 -27
  57. package/src/components/chat/{Sender → sender}/Sender.scss +278 -85
  58. package/src/components/chat/{Sender → sender}/Sender.tsx +125 -41
  59. package/src/components/chat/tools/core/ToolGroup.tsx +1 -1
  60. package/src/components/config/ConfigSectionForm.tsx +1 -1
  61. package/src/components/config/ConfigSourceSwitch.tsx +12 -34
  62. package/src/components/config/channelDefinitions.ts +2 -2
  63. package/src/components/layout/AppShell.scss +19 -0
  64. package/src/components/layout/AppShell.tsx +45 -0
  65. package/src/components/sidebar/SessionItem.scss +17 -0
  66. package/src/components/sidebar/SessionItem.tsx +21 -13
  67. package/src/hooks/chat/model-selector.ts +150 -0
  68. package/src/hooks/chat/use-chat-adapter.ts +93 -0
  69. package/src/hooks/chat/use-chat-models.tsx +126 -57
  70. package/src/hooks/chat/use-chat-permission-mode.ts +14 -10
  71. package/src/hooks/chat/use-chat-session-actions.ts +22 -13
  72. package/src/hooks/chat/use-chat-session-messages.ts +62 -10
  73. package/src/hooks/chat/use-chat-session.ts +49 -2
  74. package/src/hooks/use-app-preferences.ts +41 -0
  75. package/src/hooks/use-session-subscription.ts +101 -0
  76. package/src/hooks/use-sidebar-navigation.ts +35 -0
  77. package/src/resources/adapters.ts +20 -0
  78. package/src/resources/locales/en.json +6 -0
  79. package/src/resources/locales/zh.json +6 -0
  80. package/src/routes/AppRoutes.tsx +22 -0
  81. package/src/routes/ArchiveRoute.tsx +5 -0
  82. package/src/routes/AutomationRoute.tsx +5 -0
  83. package/src/routes/BenchmarkRoute.tsx +5 -0
  84. package/src/{components/Chat.scss → routes/ChatRoute.scss} +35 -0
  85. package/src/routes/ChatRoute.tsx +132 -0
  86. package/src/routes/ConfigRoute.tsx +5 -0
  87. package/src/routes/KnowledgeRoute.tsx +5 -0
  88. package/dist/assets/channel-DhtnrNJ6.js +0 -1
  89. package/dist/assets/clone-7bHB6YkC.js +0 -1
  90. package/dist/assets/flowDiagram-v2-4f6560a1-_13Sz5Wh.js +0 -1
  91. package/dist/assets/index-DRSI_ZIL.js +0 -514
  92. package/src/components/Chat.tsx +0 -100
  93. package/src/components/{AutomationView → automation-view}/RuleFormPanel.scss +0 -0
  94. package/src/components/{AutomationView → automation-view}/RuleFormPanel.tsx +0 -0
  95. package/src/components/{AutomationView → automation-view}/RuleSidebar.scss +0 -0
  96. package/src/components/{AutomationView → automation-view}/RuleSidebar.tsx +0 -0
  97. package/src/components/{AutomationView → automation-view}/RunHistoryPanel.scss +0 -0
  98. package/src/components/{AutomationView → automation-view}/RunHistoryPanel.tsx +0 -0
  99. package/src/components/{AutomationView → automation-view}/TaskList.scss +0 -0
  100. package/src/components/{AutomationView → automation-view}/TaskList.tsx +0 -0
  101. package/src/components/{AutomationView → automation-view}/TriggerList.scss +0 -0
  102. package/src/components/{AutomationView → automation-view}/TriggerList.tsx +0 -0
  103. package/src/components/{AutomationView/AutomationView.scss → automation-view/index.scss} +0 -0
  104. package/src/components/{AutomationView → automation-view}/types.ts +0 -0
  105. package/src/components/{BenchmarkView → benchmark-view}/BenchmarkCasePanel.scss +0 -0
  106. package/src/components/{BenchmarkView → benchmark-view}/BenchmarkCasePanel.tsx +0 -0
  107. package/src/components/{BenchmarkView → benchmark-view}/BenchmarkSidebar.scss +0 -0
  108. package/src/components/{BenchmarkView → benchmark-view}/BenchmarkSidebar.tsx +0 -0
  109. package/src/components/{BenchmarkView → benchmark-view}/BenchmarkView.scss +0 -0
  110. package/src/components/{BenchmarkView → benchmark-view}/types.ts +0 -0
  111. package/src/components/{BenchmarkView → benchmark-view}/utils.ts +0 -0
  112. package/src/components/chat/{Messages → messages}/MessageFooter.tsx +0 -0
  113. package/src/components/chat/{Messages → messages}/MessageItem.scss +0 -0
  114. package/src/components/chat/{Messages → messages}/MessageItem.tsx +0 -0
  115. package/src/components/chat/{Messages → messages}/message-utils.ts +0 -0
  116. package/src/components/chat/{Sender → sender}/CompletionMenu.scss +0 -0
  117. package/src/components/chat/{Sender → sender}/CompletionMenu.tsx +0 -0
  118. package/src/components/chat/{Sender → sender}/ThinkingStatus.scss +0 -0
  119. package/src/components/chat/{Sender → sender}/ThinkingStatus.tsx +0 -0
  120. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/EventList.scss +0 -0
  121. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/EventList.tsx +0 -0
  122. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/gantt.ts +0 -0
  123. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/git-graph.ts +0 -0
  124. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/index.scss +0 -0
  125. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/index.tsx +0 -0
  126. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/mermaid.ts +0 -0
  127. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/types.ts +0 -0
  128. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/utils.ts +0 -0
  129. package/src/components/config/{recordEditors → record-editors}/BooleanRecordEditor.scss +0 -0
  130. package/src/components/config/{recordEditors → record-editors}/BooleanRecordEditor.tsx +0 -0
  131. package/src/components/config/{recordEditors → record-editors}/ChannelRecordEditor.scss +0 -0
  132. package/src/components/config/{recordEditors → record-editors}/ChannelRecordEditor.tsx +33 -33
  133. /package/src/components/config/{recordEditors → record-editors}/KeyValueEditor.scss +0 -0
  134. /package/src/components/config/{recordEditors → record-editors}/KeyValueEditor.tsx +0 -0
  135. /package/src/components/config/{recordEditors → record-editors}/McpServersRecordEditor.scss +0 -0
  136. /package/src/components/config/{recordEditors → record-editors}/McpServersRecordEditor.tsx +0 -0
  137. /package/src/components/config/{recordEditors → record-editors}/ModelServicesRecordEditor.scss +0 -0
  138. /package/src/components/config/{recordEditors → record-editors}/ModelServicesRecordEditor.tsx +0 -0
  139. /package/src/components/config/{recordEditors → record-editors}/RecordEditors.scss +0 -0
  140. /package/src/components/config/{recordEditors → record-editors}/RecordJsonEditor.scss +0 -0
  141. /package/src/components/config/{recordEditors → record-editors}/RecordJsonEditor.tsx +0 -0
  142. /package/src/components/config/{recordEditors → record-editors}/index.tsx +0 -0
@@ -0,0 +1,93 @@
1
+ import { type ReactNode, createElement } from 'react'
2
+ import { useEffect, useMemo, useState } from 'react'
3
+ import useSWR from 'swr'
4
+
5
+ import { getConfig } from '#~/api.js'
6
+ import { getAdapterDisplay } from '#~/resources/adapters.js'
7
+ import type { ConfigResponse } from '@vibe-forge/core'
8
+
9
+ const ADAPTER_STORAGE_KEY = 'vf_chat_adapter'
10
+
11
+ export function useChatAdapter() {
12
+ const [selectedAdapter, setSelectedAdapter] = useState<string | undefined>(() => {
13
+ try {
14
+ const raw = localStorage.getItem(ADAPTER_STORAGE_KEY)
15
+ return raw == null || raw.trim() === '' ? undefined : raw
16
+ } catch {
17
+ return undefined
18
+ }
19
+ })
20
+
21
+ const { data: configRes } = useSWR<ConfigResponse>('/api/config', getConfig)
22
+
23
+ const mergedAdapters = useMemo(() => {
24
+ return configRes?.sources?.merged?.adapters ?? {}
25
+ }, [configRes?.sources?.merged?.adapters])
26
+
27
+ const defaultAdapter = configRes?.sources?.merged?.general?.defaultAdapter
28
+
29
+ const resolveAdapter = (value?: string) => {
30
+ const normalizedValue = typeof value === 'string' ? value.trim() : ''
31
+ const keys = Object.keys(mergedAdapters)
32
+ if (keys.length === 0) return undefined
33
+ if (normalizedValue !== '' && keys.includes(normalizedValue)) return normalizedValue
34
+ if (defaultAdapter && keys.includes(defaultAdapter as string)) return defaultAdapter as string
35
+ return keys[0]
36
+ }
37
+
38
+ const updateSelectedAdapter = (value?: string) => {
39
+ setSelectedAdapter((prev) => {
40
+ const nextValue = resolveAdapter(value)
41
+ return nextValue === prev ? prev : nextValue
42
+ })
43
+ }
44
+
45
+ const adapterOptions = useMemo<Array<{ value: string; label: ReactNode }>>(() => {
46
+ const keys = Object.keys(mergedAdapters)
47
+ return keys.map((key) => {
48
+ const display = getAdapterDisplay(key)
49
+ return {
50
+ value: key,
51
+ label: createElement('span', { className: 'adapter-option' }, [
52
+ display.icon != null
53
+ ? createElement('img', {
54
+ key: 'icon',
55
+ className: 'adapter-option__icon',
56
+ src: display.icon,
57
+ alt: '',
58
+ 'aria-hidden': true
59
+ })
60
+ : null,
61
+ createElement('span', { key: 'text', className: 'adapter-option__text' }, display.title)
62
+ ])
63
+ }
64
+ })
65
+ }, [mergedAdapters])
66
+
67
+ // Auto-select: use stored value if valid, else config default, else first available
68
+ useEffect(() => {
69
+ const keys = Object.keys(mergedAdapters)
70
+ if (keys.length === 0) {
71
+ setSelectedAdapter(undefined)
72
+ return
73
+ }
74
+ setSelectedAdapter((prev) => resolveAdapter(prev))
75
+ }, [defaultAdapter, mergedAdapters])
76
+
77
+ // Persist to localStorage
78
+ useEffect(() => {
79
+ try {
80
+ if (selectedAdapter == null || selectedAdapter.trim() === '') {
81
+ localStorage.removeItem(ADAPTER_STORAGE_KEY)
82
+ } else {
83
+ localStorage.setItem(ADAPTER_STORAGE_KEY, selectedAdapter)
84
+ }
85
+ } catch {}
86
+ }, [selectedAdapter])
87
+
88
+ return {
89
+ selectedAdapter,
90
+ setSelectedAdapter: updateSelectedAdapter,
91
+ adapterOptions
92
+ }
93
+ }
@@ -2,13 +2,21 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
2
2
  import { useTranslation } from 'react-i18next'
3
3
  import useSWR from 'swr'
4
4
 
5
- import type { ConfigResponse, ModelServiceConfig, RecommendedModelConfig } from '@vibe-forge/core'
6
5
  import { getConfig } from '#~/api.js'
6
+ import type { AdapterBuiltinModel, ConfigResponse, ModelServiceConfig, RecommendedModelConfig } from '@vibe-forge/core'
7
+ import {
8
+ buildServiceModelSelector,
9
+ listServiceModels,
10
+ resolveChatModelSelection,
11
+ resolveDefaultChatModelSelection,
12
+ resolveServiceModelSelector
13
+ } from './model-selector'
7
14
 
8
15
  export interface ModelSelectOption {
9
16
  value: string
10
17
  label: React.ReactNode
11
18
  searchText: string
19
+ displayLabel: string
12
20
  }
13
21
 
14
22
  export interface ModelSelectGroup {
@@ -16,7 +24,11 @@ export interface ModelSelectGroup {
16
24
  options: ModelSelectOption[]
17
25
  }
18
26
 
19
- export function useChatModels() {
27
+ export function useChatModels({
28
+ selectedAdapter
29
+ }: {
30
+ selectedAdapter?: string
31
+ } = {}) {
20
32
  const { t } = useTranslation()
21
33
  const [selectedModel, setSelectedModel] = useState<string | undefined>(() => {
22
34
  try {
@@ -41,76 +53,95 @@ export function useChatModels() {
41
53
  ))
42
54
  }, [configRes?.sources?.merged?.general?.recommendedModels])
43
55
 
44
- const modelServiceEntries = useMemo(() => Object.entries(mergedModelServices), [mergedModelServices])
56
+ const adapterBuiltinModels = useMemo(() => {
57
+ const raw = configRes?.sources?.merged?.adapterBuiltinModels
58
+ return (raw ?? {}) as Record<string, AdapterBuiltinModel[]>
59
+ }, [configRes?.sources?.merged?.adapterBuiltinModels])
45
60
 
46
- const availableModels = useMemo(() => {
47
- const list: Array<{ model: string; serviceKey: string; serviceTitle: string }> = []
48
- for (const [serviceKey, serviceValue] of modelServiceEntries) {
49
- const service = (serviceValue != null && typeof serviceValue === 'object')
50
- ? serviceValue as ModelServiceConfig
51
- : undefined
52
- const serviceTitle = service?.title?.trim() !== '' ? service?.title ?? '' : serviceKey
53
- const models = Array.isArray(service?.models) ? service?.models.filter(item => typeof item === 'string') : []
54
- for (const model of models) {
55
- list.push({ model, serviceKey, serviceTitle })
56
- }
61
+ const activeBuiltinModels = useMemo(() => {
62
+ if (selectedAdapter && adapterBuiltinModels[selectedAdapter]) {
63
+ return { [selectedAdapter]: adapterBuiltinModels[selectedAdapter] }
64
+ }
65
+ return adapterBuiltinModels
66
+ }, [adapterBuiltinModels, selectedAdapter])
67
+
68
+ const activeBuiltinModelValues = useMemo(() => (
69
+ Object.values(activeBuiltinModels).flat().map(model => model.value)
70
+ ), [activeBuiltinModels])
71
+
72
+ const builtinModelSet = useMemo(() => {
73
+ const set = new Set<string>()
74
+ for (const models of Object.values(activeBuiltinModels)) {
75
+ for (const m of models) set.add(m.value)
57
76
  }
58
- return list
59
- }, [modelServiceEntries])
77
+ return set
78
+ }, [activeBuiltinModels])
79
+
80
+ const modelServiceEntries = useMemo(() => Object.entries(mergedModelServices), [mergedModelServices])
60
81
 
61
- const availableModelValues = useMemo(() => availableModels.map(item => item.model), [availableModels])
62
- const availableModelKey = useMemo(() => availableModelValues.join('|'), [availableModelValues])
63
- const availableModelSet = useMemo(() => new Set(availableModelValues), [availableModelKey])
64
- const hasAvailableModels = availableModelValues.length > 0
82
+ const availableServiceModels = useMemo(() => listServiceModels(mergedModelServices), [mergedModelServices])
83
+ const hasAvailableModels = availableServiceModels.length > 0 || builtinModelSet.size > 0
65
84
  const modelToService = useMemo(() => {
66
85
  const map = new Map<string, { key: string; title: string }>()
67
- for (const entry of availableModels) {
86
+ for (const entry of availableServiceModels) {
87
+ const serviceValue = mergedModelServices[entry.serviceKey]
88
+ const serviceTitle = serviceValue?.title?.trim() !== '' ? serviceValue?.title ?? '' : entry.serviceKey
68
89
  if (!map.has(entry.model)) {
69
- map.set(entry.model, { key: entry.serviceKey, title: entry.serviceTitle })
90
+ map.set(entry.model, { key: entry.serviceKey, title: serviceTitle })
70
91
  }
71
92
  }
72
93
  return map
73
- }, [availableModels])
94
+ }, [availableServiceModels, mergedModelServices])
74
95
  const defaultModelService = configRes?.sources?.merged?.general?.defaultModelService
75
96
  const defaultModel = configRes?.sources?.merged?.general?.defaultModel
76
97
  const formatModelWithService = useCallback((model: string | undefined) => {
77
- const normalizedModel = typeof model === 'string' ? model.trim() : ''
78
- if (normalizedModel === '') return undefined
79
- if (normalizedModel.includes(',')) return normalizedModel
80
- const resolvedService = modelToService.get(normalizedModel)?.key ?? defaultModelService
81
- return resolvedService ? `${resolvedService},${normalizedModel}` : normalizedModel
82
- }, [defaultModelService, modelToService])
98
+ return resolveChatModelSelection({
99
+ value: model,
100
+ builtinModels: activeBuiltinModelValues,
101
+ serviceModels: availableServiceModels,
102
+ defaultModelService
103
+ })
104
+ }, [activeBuiltinModelValues, availableServiceModels, defaultModelService])
83
105
  const resolvedDefaultModel = useMemo(() => {
84
- if (!hasAvailableModels) return undefined
85
- if (defaultModel && availableModelSet.has(defaultModel)) return defaultModel
86
- if (defaultModelService && mergedModelServices[defaultModelService]) {
87
- const service = mergedModelServices[defaultModelService]
88
- const models = Array.isArray(service?.models) ? service?.models.filter(item => typeof item === 'string') : []
89
- if (models.length > 0) return models[0]
90
- }
91
- return availableModelValues[0]
106
+ return resolveDefaultChatModelSelection({
107
+ defaultModel,
108
+ defaultModelService,
109
+ builtinModels: activeBuiltinModelValues,
110
+ serviceModels: availableServiceModels
111
+ })
92
112
  }, [
93
- availableModelSet,
94
- availableModelValues,
113
+ activeBuiltinModelValues,
114
+ availableServiceModels,
95
115
  defaultModel,
96
- defaultModelService,
97
- hasAvailableModels,
98
- mergedModelServices
116
+ defaultModelService
99
117
  ])
100
118
  const selectedModelWithService = useMemo(() => (
101
- formatModelWithService(selectedModel)
102
- ), [formatModelWithService, selectedModel])
119
+ formatModelWithService(selectedModel) ?? resolvedDefaultModel
120
+ ), [formatModelWithService, resolvedDefaultModel, selectedModel])
121
+
122
+ const resolveSelectableModel = useCallback((value?: string) => {
123
+ return resolveChatModelSelection({
124
+ value,
125
+ builtinModels: activeBuiltinModelValues,
126
+ serviceModels: availableServiceModels,
127
+ defaultModelService
128
+ }) ?? resolvedDefaultModel
129
+ }, [activeBuiltinModelValues, availableServiceModels, defaultModelService, resolvedDefaultModel])
130
+
131
+ const updateSelectedModel = useCallback((value?: string) => {
132
+ setSelectedModel((prev) => {
133
+ const nextValue = resolveSelectableModel(value)
134
+ return nextValue === prev ? prev : nextValue
135
+ })
136
+ }, [resolveSelectableModel])
103
137
 
104
138
  useEffect(() => {
105
139
  if (!hasAvailableModels) {
106
140
  setSelectedModel(undefined)
107
141
  return
108
142
  }
109
- setSelectedModel((prev) => {
110
- if (prev != null && availableModelSet.has(prev)) return prev
111
- return resolvedDefaultModel
112
- })
113
- }, [availableModelSet, hasAvailableModels, resolvedDefaultModel])
143
+ setSelectedModel((prev) => resolveSelectableModel(prev))
144
+ }, [hasAvailableModels, resolveSelectableModel, selectedAdapter])
114
145
 
115
146
  useEffect(() => {
116
147
  try {
@@ -150,7 +181,8 @@ export function useChatModels() {
150
181
  return {
151
182
  value: params.value,
152
183
  label,
153
- searchText
184
+ searchText,
185
+ displayLabel: params.title
154
186
  }
155
187
  }
156
188
 
@@ -171,14 +203,16 @@ export function useChatModels() {
171
203
  const serviceTitle = service?.title?.trim() !== '' ? service?.title ?? '' : serviceKey
172
204
  const groupTitle = serviceTitle?.trim() !== '' ? serviceTitle : serviceKey
173
205
  const serviceDescription = service?.description
174
- const models = Array.isArray(service?.models) ? service?.models.filter(item => typeof item === 'string') : []
206
+ const models = Array.isArray(service?.models)
207
+ ? service.models.filter((item): item is string => typeof item === 'string')
208
+ : []
175
209
  if (models.length === 0) return null
176
- const options = models.map((model) => {
210
+ const options = models.map((model: string) => {
177
211
  const alias = resolveFirstAlias(service?.modelsAlias as Record<string, string[]> | undefined, model)
178
212
  const title = alias ?? model
179
213
  const description = alias ? model : serviceTitle
180
214
  return buildOption({
181
- value: model,
215
+ value: buildServiceModelSelector(serviceKey, model),
182
216
  title,
183
217
  description,
184
218
  serviceKey,
@@ -200,7 +234,11 @@ export function useChatModels() {
200
234
  const recommendedOptions = recommendedModels
201
235
  .filter((item) => {
202
236
  if (item.placement && item.placement !== 'modelSelector') return false
203
- return availableModelSet.has(item.model)
237
+ return resolveServiceModelSelector({
238
+ value: item.service ? buildServiceModelSelector(item.service, item.model) : item.model,
239
+ serviceModels: availableServiceModels,
240
+ preferredServiceKey: item.service ?? defaultModelService
241
+ }) != null
204
242
  })
205
243
  .map((item) => {
206
244
  const serviceInfo = item.service ? mergedModelServices[item.service] : undefined
@@ -214,8 +252,13 @@ export function useChatModels() {
214
252
  const description = item.description?.trim() !== ''
215
253
  ? item.description
216
254
  : serviceTitle
255
+ const value = resolveServiceModelSelector({
256
+ value: item.service ? buildServiceModelSelector(item.service, item.model) : item.model,
257
+ serviceModels: availableServiceModels,
258
+ preferredServiceKey: item.service ?? defaultModelService
259
+ }) ?? item.model
217
260
  return buildOption({
218
- value: item.model,
261
+ value,
219
262
  title,
220
263
  description,
221
264
  serviceKey: item.service ?? modelToService.get(item.model)?.key,
@@ -235,9 +278,35 @@ export function useChatModels() {
235
278
  options: recommendedOptions
236
279
  })
237
280
  }
281
+
282
+ // Adapter builtin model groups (filtered to active adapter)
283
+ for (const [adapterKey, models] of Object.entries(activeBuiltinModels)) {
284
+ if (!Array.isArray(models) || models.length === 0) continue
285
+ const adapterTitle = t('chat.modelGroupBuiltin', {
286
+ adapter: adapterKey,
287
+ defaultValue: `${adapterKey} (Default)`
288
+ })
289
+ groups.push({
290
+ label: (
291
+ <div className='model-group-label'>
292
+ <div className='model-group-title'>{adapterTitle}</div>
293
+ </div>
294
+ ),
295
+ options: models.map(m =>
296
+ buildOption({
297
+ value: m.value,
298
+ title: m.title,
299
+ description: m.description
300
+ })
301
+ )
302
+ })
303
+ }
304
+
238
305
  return [...groups, ...serviceGroups]
239
306
  }, [
240
- availableModelSet,
307
+ activeBuiltinModels,
308
+ availableServiceModels,
309
+ defaultModelService,
241
310
  modelToService,
242
311
  mergedModelServices,
243
312
  modelServiceEntries,
@@ -248,7 +317,7 @@ export function useChatModels() {
248
317
  return {
249
318
  selectedModel,
250
319
  selectedModelWithService,
251
- setSelectedModel,
320
+ setSelectedModel: updateSelectedModel,
252
321
  modelOptions,
253
322
  hasAvailableModels
254
323
  }
@@ -5,22 +5,26 @@ export type PermissionMode = 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'b
5
5
 
6
6
  const PERMISSION_MODE_STORAGE_KEY = 'vf_chat_permission_mode'
7
7
 
8
- const isPermissionMode = (value: string): value is PermissionMode => {
9
- return value === 'default'
10
- || value === 'acceptEdits'
11
- || value === 'plan'
12
- || value === 'dontAsk'
13
- || value === 'bypassPermissions'
8
+ export const isPermissionMode = (value: string): value is PermissionMode => {
9
+ return value === 'default' ||
10
+ value === 'acceptEdits' ||
11
+ value === 'plan' ||
12
+ value === 'dontAsk' ||
13
+ value === 'bypassPermissions'
14
14
  }
15
15
 
16
16
  export function useChatPermissionMode() {
17
17
  const [permissionMode, setPermissionMode] = useState<PermissionMode>('default')
18
18
 
19
+ const updatePermissionMode = (value?: string) => {
20
+ setPermissionMode(isPermissionMode(value ?? '') ? value : 'default')
21
+ }
22
+
19
23
  useEffect(() => {
20
24
  try {
21
25
  const raw = localStorage.getItem(PERMISSION_MODE_STORAGE_KEY)
22
26
  if (raw && isPermissionMode(raw)) {
23
- setPermissionMode(raw)
27
+ updatePermissionMode(raw)
24
28
  }
25
29
  } catch {}
26
30
  }, [])
@@ -31,17 +35,17 @@ export function useChatPermissionMode() {
31
35
  } catch {}
32
36
  }, [permissionMode])
33
37
 
34
- const permissionModeOptions = useMemo<Array<{ value: PermissionMode; label: ReactNode }>>(() => ([
38
+ const permissionModeOptions = useMemo<Array<{ value: PermissionMode; label: ReactNode }>>(() => [
35
39
  { value: 'default', label: '默认' },
36
40
  { value: 'acceptEdits', label: '接受编辑' },
37
41
  { value: 'plan', label: '计划' },
38
42
  { value: 'dontAsk', label: '不询问' },
39
43
  { value: 'bypassPermissions', label: '跳过权限' }
40
- ]), [])
44
+ ], [])
41
45
 
42
46
  return {
43
47
  permissionMode,
44
- setPermissionMode,
48
+ setPermissionMode: updatePermissionMode,
45
49
  permissionModeOptions
46
50
  }
47
51
  }
@@ -4,9 +4,9 @@ import { useTranslation } from 'react-i18next'
4
4
  import { useNavigate } from 'react-router-dom'
5
5
  import { useSWRConfig } from 'swr'
6
6
 
7
- import type { ChatMessageContent, Session } from '@vibe-forge/core'
8
- import { createSession } from '#~/api.js'
7
+ import { createSession, getApiErrorMessage } from '#~/api.js'
9
8
  import { connectionManager } from '#~/connectionManager.js'
9
+ import type { ChatMessageContent, Session } from '@vibe-forge/core'
10
10
  import type { PermissionMode } from './use-chat-permission-mode'
11
11
 
12
12
  export function useChatSessionActions({
@@ -14,12 +14,14 @@ export function useChatSessionActions({
14
14
  modelForQuery,
15
15
  hasAvailableModels,
16
16
  permissionMode,
17
+ adapter,
17
18
  onClearMessages
18
19
  }: {
19
20
  session?: Session
20
21
  modelForQuery?: string
21
22
  hasAvailableModels: boolean
22
23
  permissionMode: PermissionMode
24
+ adapter?: string
23
25
  onClearMessages: () => void
24
26
  }) {
25
27
  const { message } = App.useApp()
@@ -30,17 +32,18 @@ export function useChatSessionActions({
30
32
  const isThinking = isCreating || session?.status === 'running'
31
33
 
32
34
  const send = useCallback(async (text: string) => {
33
- if (text.trim() === '' || isThinking) return
35
+ if (text.trim() === '' || isThinking) return false
34
36
  if (!hasAvailableModels) {
35
37
  void message.warning(t('chat.modelConfigRequired'))
36
- return
38
+ return false
37
39
  }
38
40
 
39
41
  if (!session?.id) {
40
42
  setIsCreating(true)
41
43
  try {
42
44
  const { session: newSession } = await createSession(undefined, text.trim(), undefined, modelForQuery, {
43
- permissionMode
45
+ permissionMode,
46
+ adapter
44
47
  })
45
48
 
46
49
  await mutate('/api/sessions', (prev: { sessions: Session[] } | undefined) => {
@@ -52,19 +55,22 @@ export function useChatSessionActions({
52
55
  }, false)
53
56
 
54
57
  void navigate(`/session/${newSession.id}`)
58
+ return true
55
59
  } catch (err) {
56
60
  console.error(err)
57
61
  setIsCreating(false)
58
- void message.error('Failed to create session')
62
+ void message.error(getApiErrorMessage(err, 'Failed to create session'))
63
+ return false
59
64
  }
60
- return
61
65
  }
62
66
 
63
67
  connectionManager.send(session.id, {
64
68
  type: 'user_message',
65
69
  text: text.trim()
66
70
  })
71
+ return true
67
72
  }, [
73
+ adapter,
68
74
  hasAvailableModels,
69
75
  isThinking,
70
76
  message,
@@ -77,17 +83,18 @@ export function useChatSessionActions({
77
83
  ])
78
84
 
79
85
  const sendContent = useCallback(async (content: ChatMessageContent[]) => {
80
- if (content.length === 0 || isThinking) return
86
+ if (content.length === 0 || isThinking) return false
81
87
  if (!hasAvailableModels) {
82
88
  void message.warning(t('chat.modelConfigRequired'))
83
- return
89
+ return false
84
90
  }
85
91
 
86
92
  if (!session?.id) {
87
93
  setIsCreating(true)
88
94
  try {
89
95
  const { session: newSession } = await createSession(undefined, undefined, content, modelForQuery, {
90
- permissionMode
96
+ permissionMode,
97
+ adapter
91
98
  })
92
99
 
93
100
  await mutate('/api/sessions', (prev: { sessions: Session[] } | undefined) => {
@@ -99,20 +106,22 @@ export function useChatSessionActions({
99
106
  }, false)
100
107
 
101
108
  void navigate(`/session/${newSession.id}`)
102
- setIsCreating(false)
109
+ return true
103
110
  } catch (err) {
104
111
  console.error(err)
105
112
  setIsCreating(false)
106
- void message.error('Failed to create session')
113
+ void message.error(getApiErrorMessage(err, 'Failed to create session'))
114
+ return false
107
115
  }
108
- return
109
116
  }
110
117
 
111
118
  connectionManager.send(session.id, {
112
119
  type: 'user_message',
113
120
  content
114
121
  })
122
+ return true
115
123
  }, [
124
+ adapter,
116
125
  hasAvailableModels,
117
126
  isThinking,
118
127
  message,