@vibe-forge/client 0.2.0-alpha.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 (184) hide show
  1. package/LICENSE +21 -0
  2. package/cli.cjs +6 -0
  3. package/index.html +27 -0
  4. package/package.json +42 -0
  5. package/src/App.tsx +174 -0
  6. package/src/api.ts +241 -0
  7. package/src/components/ArchiveView.scss +168 -0
  8. package/src/components/ArchiveView.tsx +299 -0
  9. package/src/components/AutomationView/AutomationView.scss +26 -0
  10. package/src/components/AutomationView/RuleFormPanel.scss +129 -0
  11. package/src/components/AutomationView/RuleFormPanel.tsx +257 -0
  12. package/src/components/AutomationView/RuleSidebar.scss +219 -0
  13. package/src/components/AutomationView/RuleSidebar.tsx +258 -0
  14. package/src/components/AutomationView/RunHistoryPanel.scss +286 -0
  15. package/src/components/AutomationView/RunHistoryPanel.tsx +320 -0
  16. package/src/components/AutomationView/TaskList.scss +128 -0
  17. package/src/components/AutomationView/TaskList.tsx +79 -0
  18. package/src/components/AutomationView/TriggerList.scss +153 -0
  19. package/src/components/AutomationView/TriggerList.tsx +217 -0
  20. package/src/components/AutomationView/index.tsx +228 -0
  21. package/src/components/AutomationView/types.ts +21 -0
  22. package/src/components/Chat.scss +89 -0
  23. package/src/components/Chat.tsx +92 -0
  24. package/src/components/ConfigView.scss +185 -0
  25. package/src/components/ConfigView.tsx +258 -0
  26. package/src/components/NavRail.scss +71 -0
  27. package/src/components/NavRail.tsx +188 -0
  28. package/src/components/Sidebar.scss +112 -0
  29. package/src/components/Sidebar.tsx +291 -0
  30. package/src/components/chat/ChatHeader.scss +401 -0
  31. package/src/components/chat/ChatHeader.tsx +342 -0
  32. package/src/components/chat/ChatHistoryView.tsx +122 -0
  33. package/src/components/chat/ChatSettingsView.tsx +22 -0
  34. package/src/components/chat/ChatTimelineView.scss +53 -0
  35. package/src/components/chat/ChatTimelineView.tsx +158 -0
  36. package/src/components/chat/CodeBlock.scss +87 -0
  37. package/src/components/chat/CodeBlock.tsx +179 -0
  38. package/src/components/chat/CompletionMenu.scss +70 -0
  39. package/src/components/chat/CompletionMenu.tsx +58 -0
  40. package/src/components/chat/CurrentTodoList.scss +217 -0
  41. package/src/components/chat/CurrentTodoList.tsx +103 -0
  42. package/src/components/chat/MarkdownContent.tsx +43 -0
  43. package/src/components/chat/MessageFooter.tsx +48 -0
  44. package/src/components/chat/MessageItem.scss +251 -0
  45. package/src/components/chat/MessageItem.tsx +78 -0
  46. package/src/components/chat/NewSessionGuide.scss +186 -0
  47. package/src/components/chat/NewSessionGuide.tsx +167 -0
  48. package/src/components/chat/Sender.scss +367 -0
  49. package/src/components/chat/Sender.tsx +541 -0
  50. package/src/components/chat/SessionTimelinePanel/EventList.scss +58 -0
  51. package/src/components/chat/SessionTimelinePanel/EventList.tsx +212 -0
  52. package/src/components/chat/SessionTimelinePanel/gantt.ts +177 -0
  53. package/src/components/chat/SessionTimelinePanel/git-graph.ts +518 -0
  54. package/src/components/chat/SessionTimelinePanel/index.scss +28 -0
  55. package/src/components/chat/SessionTimelinePanel/index.tsx +121 -0
  56. package/src/components/chat/SessionTimelinePanel/mermaid.ts +4 -0
  57. package/src/components/chat/SessionTimelinePanel/types.ts +64 -0
  58. package/src/components/chat/SessionTimelinePanel/utils.ts +20 -0
  59. package/src/components/chat/ThinkingStatus.scss +70 -0
  60. package/src/components/chat/ThinkingStatus.tsx +13 -0
  61. package/src/components/chat/ToolCallBox.scss +137 -0
  62. package/src/components/chat/ToolCallBox.tsx +55 -0
  63. package/src/components/chat/ToolGroup.scss +154 -0
  64. package/src/components/chat/ToolGroup.tsx +102 -0
  65. package/src/components/chat/ToolRenderer.tsx +45 -0
  66. package/src/components/chat/messageUtils.ts +171 -0
  67. package/src/components/chat/safeSerialize.ts +84 -0
  68. package/src/components/chat/tools/DefaultTool.tsx +63 -0
  69. package/src/components/chat/tools/adapter-claude/BashTool.scss +71 -0
  70. package/src/components/chat/tools/adapter-claude/BashTool.tsx +82 -0
  71. package/src/components/chat/tools/adapter-claude/GlobTool.scss +88 -0
  72. package/src/components/chat/tools/adapter-claude/GlobTool.tsx +85 -0
  73. package/src/components/chat/tools/adapter-claude/GrepTool.scss +96 -0
  74. package/src/components/chat/tools/adapter-claude/GrepTool.tsx +114 -0
  75. package/src/components/chat/tools/adapter-claude/LSTool.scss +85 -0
  76. package/src/components/chat/tools/adapter-claude/LSTool.tsx +94 -0
  77. package/src/components/chat/tools/adapter-claude/ReadTool.scss +57 -0
  78. package/src/components/chat/tools/adapter-claude/ReadTool.tsx +87 -0
  79. package/src/components/chat/tools/adapter-claude/TodoTool.scss +78 -0
  80. package/src/components/chat/tools/adapter-claude/TodoTool.tsx +60 -0
  81. package/src/components/chat/tools/adapter-claude/WriteTool.scss +92 -0
  82. package/src/components/chat/tools/adapter-claude/WriteTool.tsx +86 -0
  83. package/src/components/chat/tools/adapter-claude/components/FileList.scss +65 -0
  84. package/src/components/chat/tools/adapter-claude/components/FileList.tsx +185 -0
  85. package/src/components/chat/tools/adapter-claude/index.ts +28 -0
  86. package/src/components/chat/tools/defineToolRender.ts +28 -0
  87. package/src/components/chat/tools/task/GetTaskInfoTool.scss +50 -0
  88. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +88 -0
  89. package/src/components/chat/tools/task/ListTasksTool.scss +56 -0
  90. package/src/components/chat/tools/task/ListTasksTool.tsx +83 -0
  91. package/src/components/chat/tools/task/StartTasksTool.scss +56 -0
  92. package/src/components/chat/tools/task/StartTasksTool.tsx +96 -0
  93. package/src/components/chat/tools/task/components/TaskToolCard.scss +127 -0
  94. package/src/components/chat/tools/task/components/TaskToolCard.tsx +177 -0
  95. package/src/components/chat/tools/task/index.ts +15 -0
  96. package/src/components/chat/useChatModels.tsx +206 -0
  97. package/src/components/chat/useChatSession.ts +370 -0
  98. package/src/components/config/ConfigAboutSection.scss +111 -0
  99. package/src/components/config/ConfigAboutSection.tsx +86 -0
  100. package/src/components/config/ConfigDisplayValue.scss +22 -0
  101. package/src/components/config/ConfigDisplayValue.tsx +62 -0
  102. package/src/components/config/ConfigEditors.scss +65 -0
  103. package/src/components/config/ConfigEditors.tsx +98 -0
  104. package/src/components/config/ConfigFieldRow.scss +97 -0
  105. package/src/components/config/ConfigFieldRow.tsx +36 -0
  106. package/src/components/config/ConfigSectionForm.scss +94 -0
  107. package/src/components/config/ConfigSectionForm.tsx +436 -0
  108. package/src/components/config/ConfigSectionPanel.tsx +67 -0
  109. package/src/components/config/ConfigShortcutInput.scss +11 -0
  110. package/src/components/config/ConfigShortcutInput.tsx +52 -0
  111. package/src/components/config/ConfigSourceSwitch.tsx +57 -0
  112. package/src/components/config/configSchema.ts +319 -0
  113. package/src/components/config/configUtils.ts +83 -0
  114. package/src/components/config/index.tsx +5 -0
  115. package/src/components/config/recordEditors/BooleanRecordEditor.scss +1 -0
  116. package/src/components/config/recordEditors/BooleanRecordEditor.tsx +75 -0
  117. package/src/components/config/recordEditors/KeyValueEditor.scss +1 -0
  118. package/src/components/config/recordEditors/KeyValueEditor.tsx +97 -0
  119. package/src/components/config/recordEditors/McpServersRecordEditor.scss +1 -0
  120. package/src/components/config/recordEditors/McpServersRecordEditor.tsx +258 -0
  121. package/src/components/config/recordEditors/ModelServicesRecordEditor.scss +1 -0
  122. package/src/components/config/recordEditors/ModelServicesRecordEditor.tsx +233 -0
  123. package/src/components/config/recordEditors/RecordEditors.scss +117 -0
  124. package/src/components/config/recordEditors/RecordJsonEditor.scss +1 -0
  125. package/src/components/config/recordEditors/RecordJsonEditor.tsx +113 -0
  126. package/src/components/config/recordEditors/index.tsx +5 -0
  127. package/src/components/knowledge-base/KnowledgeBaseView.scss +19 -0
  128. package/src/components/knowledge-base/KnowledgeBaseView.tsx +186 -0
  129. package/src/components/knowledge-base/components/ActionButton.scss +5 -0
  130. package/src/components/knowledge-base/components/ActionButton.tsx +9 -0
  131. package/src/components/knowledge-base/components/EmptyState.scss +19 -0
  132. package/src/components/knowledge-base/components/EmptyState.tsx +42 -0
  133. package/src/components/knowledge-base/components/EntitiesTab.scss +5 -0
  134. package/src/components/knowledge-base/components/EntitiesTab.tsx +80 -0
  135. package/src/components/knowledge-base/components/EntityItem.scss +82 -0
  136. package/src/components/knowledge-base/components/EntityItem.tsx +79 -0
  137. package/src/components/knowledge-base/components/EntityList.scss +5 -0
  138. package/src/components/knowledge-base/components/EntityList.tsx +70 -0
  139. package/src/components/knowledge-base/components/FilterBar.scss +21 -0
  140. package/src/components/knowledge-base/components/FilterBar.tsx +51 -0
  141. package/src/components/knowledge-base/components/FlowsTab.scss +5 -0
  142. package/src/components/knowledge-base/components/FlowsTab.tsx +80 -0
  143. package/src/components/knowledge-base/components/KnowledgeBaseHeader.scss +27 -0
  144. package/src/components/knowledge-base/components/KnowledgeBaseHeader.tsx +29 -0
  145. package/src/components/knowledge-base/components/KnowledgeList.scss +19 -0
  146. package/src/components/knowledge-base/components/KnowledgeList.tsx +19 -0
  147. package/src/components/knowledge-base/components/LoadingState.scss +5 -0
  148. package/src/components/knowledge-base/components/LoadingState.tsx +11 -0
  149. package/src/components/knowledge-base/components/MetaList.scss +19 -0
  150. package/src/components/knowledge-base/components/MetaList.tsx +18 -0
  151. package/src/components/knowledge-base/components/RulesTab.scss +5 -0
  152. package/src/components/knowledge-base/components/RulesTab.tsx +49 -0
  153. package/src/components/knowledge-base/components/SectionHeader.scss +22 -0
  154. package/src/components/knowledge-base/components/SectionHeader.tsx +21 -0
  155. package/src/components/knowledge-base/components/SkillsTab.scss +5 -0
  156. package/src/components/knowledge-base/components/SkillsTab.tsx +49 -0
  157. package/src/components/knowledge-base/components/SpecItem.scss +138 -0
  158. package/src/components/knowledge-base/components/SpecItem.tsx +131 -0
  159. package/src/components/knowledge-base/components/SpecList.scss +5 -0
  160. package/src/components/knowledge-base/components/SpecList.tsx +70 -0
  161. package/src/components/knowledge-base/components/TabContent.scss +8 -0
  162. package/src/components/knowledge-base/components/TabContent.tsx +17 -0
  163. package/src/components/knowledge-base/components/TabLabel.scss +10 -0
  164. package/src/components/knowledge-base/components/TabLabel.tsx +15 -0
  165. package/src/components/knowledge-base/index.tsx +1 -0
  166. package/src/components/sidebar/SessionItem.scss +256 -0
  167. package/src/components/sidebar/SessionItem.tsx +265 -0
  168. package/src/components/sidebar/SessionList.scss +92 -0
  169. package/src/components/sidebar/SessionList.tsx +166 -0
  170. package/src/components/sidebar/SidebarHeader.scss +79 -0
  171. package/src/components/sidebar/SidebarHeader.tsx +128 -0
  172. package/src/connectionManager.ts +172 -0
  173. package/src/hooks/useGlobalShortcut.ts +26 -0
  174. package/src/hooks/useQueryParams.ts +54 -0
  175. package/src/i18n.ts +22 -0
  176. package/src/main.tsx +41 -0
  177. package/src/resources/locales/en.json +765 -0
  178. package/src/resources/locales/zh.json +766 -0
  179. package/src/store/index.ts +23 -0
  180. package/src/styles/global.scss +100 -0
  181. package/src/utils/shortcutUtils.ts +88 -0
  182. package/src/vite-env.d.ts +12 -0
  183. package/src/ws.ts +33 -0
  184. package/vite.config.ts +26 -0
@@ -0,0 +1,436 @@
1
+ import './ConfigSectionForm.scss'
2
+
3
+ import { Collapse, Empty, Input, InputNumber, Select, Slider, Switch } from 'antd'
4
+ import type { ReactNode } from 'react'
5
+
6
+ import { ComplexTextEditor, StringArrayEditor } from './ConfigEditors'
7
+ import { FieldRow } from './ConfigFieldRow'
8
+ import { ShortcutInput } from './ConfigShortcutInput'
9
+ import type { FieldSpec } from './configSchema'
10
+ import { configGroupMeta, configSchema } from './configSchema'
11
+ import {
12
+ getFieldDescription,
13
+ getFieldLabel,
14
+ getTypeIcon,
15
+ getValueByPath,
16
+ getValueType,
17
+ isEmptyValue,
18
+ setValueByPath
19
+ } from './configUtils'
20
+ import type { TranslationFn } from './configUtils'
21
+ import {
22
+ BooleanRecordEditor,
23
+ KeyValueEditor,
24
+ McpServersRecordEditor,
25
+ ModelServicesRecordEditor,
26
+ RecordJsonEditor
27
+ } from './recordEditors/index'
28
+
29
+ export const SectionForm = ({
30
+ sectionKey,
31
+ fields: providedFields,
32
+ value,
33
+ onChange,
34
+ mergedModelServices,
35
+ mergedAdapters,
36
+ selectedModelService,
37
+ t
38
+ }: {
39
+ sectionKey: string
40
+ fields?: FieldSpec[]
41
+ value: unknown
42
+ onChange: (nextValue: unknown) => void
43
+ mergedModelServices: Record<string, unknown>
44
+ mergedAdapters: Record<string, unknown>
45
+ selectedModelService?: string
46
+ t: TranslationFn
47
+ }) => {
48
+ const fields = providedFields ?? configSchema[sectionKey] ?? []
49
+ if (fields.length === 0) {
50
+ return <Empty description={t('common.noData')} image={null} />
51
+ }
52
+ const directRecordSections = new Set(['modelServices', 'adapters', 'plugins', 'mcp'])
53
+
54
+ const modelServiceEntries = Object.entries(mergedModelServices)
55
+ const modelServiceOptions: Array<{ value: string; label: ReactNode }> = modelServiceEntries.map(([key, entry]) => {
56
+ const record = (entry != null && typeof entry === 'object') ? entry as Record<string, unknown> : {}
57
+ const title = typeof record.title === 'string' && record.title.trim() !== '' ? record.title : key
58
+ const description = typeof record.description === 'string' ? record.description : ''
59
+ return {
60
+ value: key,
61
+ label: (
62
+ <div className='config-view__option'>
63
+ <div className='config-view__option-title'>{title}</div>
64
+ {description !== '' && (
65
+ <div className='config-view__option-desc'>{description}</div>
66
+ )}
67
+ </div>
68
+ )
69
+ }
70
+ })
71
+ const selectedServiceRecord = selectedModelService
72
+ ? mergedModelServices[selectedModelService]
73
+ : undefined
74
+ const selectedServiceModels = (selectedServiceRecord != null && typeof selectedServiceRecord === 'object')
75
+ ? (selectedServiceRecord as Record<string, unknown>).models
76
+ : undefined
77
+ const modelOptions: Array<{ value: string; label: ReactNode }> = Array.isArray(selectedServiceModels)
78
+ ? selectedServiceModels.filter(item => typeof item === 'string').map(item => ({
79
+ value: item,
80
+ label: <span>{item}</span>
81
+ }))
82
+ : []
83
+ const adapterOptions: Array<{ value: string; label: ReactNode }> = Object.keys(mergedAdapters)
84
+ .map(key => ({
85
+ value: key,
86
+ label: <span>{key}</span>
87
+ }))
88
+
89
+ const groupedFields = fields.reduce<Record<string, FieldSpec[]>>((acc, field) => {
90
+ const key = field.group ?? 'default'
91
+ if (!acc[key]) acc[key] = []
92
+ acc[key].push(field)
93
+ return acc
94
+ }, {})
95
+ const orderedGroups = ['base', 'permissions', 'env', 'items', 'default'].filter(key => groupedFields[key]?.length)
96
+ const notificationEventOrder = ['completed', 'failed', 'terminated', 'waiting_input']
97
+
98
+ const getRecordKeyPlaceholder = (field: FieldSpec) => {
99
+ if (sectionKey === 'modelServices') return t('config.editor.newModelServiceName')
100
+ if (sectionKey === 'adapters') return t('config.editor.newAdapterName')
101
+ if (sectionKey === 'plugins') {
102
+ if (field.path.join('.') === 'extraKnownMarketplaces') return t('config.editor.newMarketplaceName')
103
+ return t('config.editor.newPluginName')
104
+ }
105
+ if (sectionKey === 'mcp') return t('config.editor.newMcpServerName')
106
+ if (sectionKey === 'general' && field.path.join('.') === 'env') return t('config.editor.newEnvVarName')
107
+ return t('config.editor.fieldKey')
108
+ }
109
+
110
+ const renderField = (field: FieldSpec) => {
111
+ const fieldValue = getValueByPath(value, field.path)
112
+ const valueToUse = fieldValue !== undefined ? fieldValue : field.defaultValue
113
+ const label = field.labelKey
114
+ ? t(field.labelKey)
115
+ : getFieldLabel(t, sectionKey, field.path, field.path[field.path.length - 1] ?? '')
116
+ const description = field.descriptionKey
117
+ ? t(field.descriptionKey)
118
+ : getFieldDescription(t, sectionKey, field.path)
119
+ const icon = field.icon ?? getTypeIcon(getValueType(valueToUse))
120
+
121
+ const handleValueChange = (nextValue: unknown) => {
122
+ const nextSectionValue = setValueByPath(value, field.path, nextValue)
123
+ onChange(nextSectionValue)
124
+ }
125
+
126
+ let control: ReactNode = null
127
+ const isStacked = ['multiline', 'json', 'record', 'string[]'].includes(field.type)
128
+
129
+ if (field.type === 'string') {
130
+ const placeholder = field.placeholderKey ? t(field.placeholderKey) : undefined
131
+ if (field.sensitive === true) {
132
+ control = (
133
+ <Input.Password
134
+ value={typeof valueToUse === 'string' ? valueToUse : ''}
135
+ onChange={(event) => handleValueChange(event.target.value)}
136
+ placeholder={t('config.editor.secretPlaceholder')}
137
+ />
138
+ )
139
+ } else {
140
+ control = (
141
+ <Input
142
+ value={typeof valueToUse === 'string' ? valueToUse : ''}
143
+ onChange={(event) => handleValueChange(event.target.value)}
144
+ placeholder={placeholder}
145
+ />
146
+ )
147
+ }
148
+ } else if (field.type === 'multiline') {
149
+ const currentText = typeof valueToUse === 'string' ? valueToUse : ''
150
+ const placeholder = field.placeholderKey ? t(field.placeholderKey) : t('config.editor.multilinePlaceholder')
151
+ control = (
152
+ <div className='config-view__multiline'>
153
+ <Input.TextArea
154
+ className='config-view__multiline-input'
155
+ value={currentText}
156
+ onChange={(event) => handleValueChange(event.target.value)}
157
+ autoSize={{ minRows: 3 }}
158
+ placeholder={placeholder}
159
+ />
160
+ </div>
161
+ )
162
+ } else if (field.type === 'number') {
163
+ const isNotificationVolume = field.path.join('.') === 'notifications.volume'
164
+ if (isNotificationVolume) {
165
+ const sliderValue = typeof valueToUse === 'number' ? valueToUse : 0
166
+ control = (
167
+ <Slider
168
+ className='config-view__slider'
169
+ min={0}
170
+ max={100}
171
+ step={1}
172
+ value={sliderValue}
173
+ onChange={(next) => handleValueChange(typeof next === 'number' ? next : 0)}
174
+ />
175
+ )
176
+ } else {
177
+ control = (
178
+ <InputNumber
179
+ value={typeof valueToUse === 'number' ? valueToUse : 0}
180
+ onChange={(next) => handleValueChange(typeof next === 'number' ? next : 0)}
181
+ />
182
+ )
183
+ }
184
+ } else if (field.type === 'boolean') {
185
+ control = (
186
+ <Switch
187
+ checked={Boolean(valueToUse)}
188
+ onChange={(next) => handleValueChange(next)}
189
+ />
190
+ )
191
+ } else if (field.type === 'string[]') {
192
+ const current = Array.isArray(valueToUse) ? valueToUse.filter(item => typeof item === 'string') : []
193
+ control = (
194
+ <StringArrayEditor
195
+ value={current}
196
+ onChange={(next) => handleValueChange(next)}
197
+ t={t}
198
+ />
199
+ )
200
+ } else if (field.type === 'select') {
201
+ const isDefaultAdapter = sectionKey === 'general' && field.path.join('.') === 'defaultAdapter'
202
+ const isDefaultModelService = sectionKey === 'general' && field.path.join('.') === 'defaultModelService'
203
+ const isDefaultModel = sectionKey === 'general' && field.path.join('.') === 'defaultModel'
204
+ const options: Array<{ value: string; label: ReactNode }> = isDefaultModelService
205
+ ? modelServiceOptions
206
+ : isDefaultModel
207
+ ? modelOptions
208
+ : isDefaultAdapter
209
+ ? adapterOptions
210
+ : (field.options ?? []).map(option => ({
211
+ value: option.value,
212
+ label: <span>{t(option.label)}</span>
213
+ }))
214
+ control = (
215
+ <Select
216
+ value={typeof valueToUse === 'string' && valueToUse !== '' ? valueToUse : undefined}
217
+ options={options}
218
+ onChange={(next) => handleValueChange(next)}
219
+ allowClear
220
+ disabled={isDefaultModel && modelOptions.length === 0}
221
+ placeholder={t(
222
+ isDefaultAdapter
223
+ ? 'config.editor.defaultAdapterPlaceholder'
224
+ : isDefaultModelService
225
+ ? 'config.editor.defaultModelServicePlaceholder'
226
+ : 'config.editor.defaultModelPlaceholder'
227
+ )}
228
+ />
229
+ )
230
+ } else if (field.type === 'json') {
231
+ control = (
232
+ <ComplexTextEditor
233
+ value={valueToUse}
234
+ onChange={handleValueChange}
235
+ />
236
+ )
237
+ } else if (field.type === 'record') {
238
+ const recordValue = (valueToUse != null && typeof valueToUse === 'object')
239
+ ? valueToUse as Record<string, unknown>
240
+ : {}
241
+ if (field.recordKind === 'modelServices') {
242
+ control = (
243
+ <ModelServicesRecordEditor
244
+ value={recordValue}
245
+ onChange={handleValueChange}
246
+ t={t}
247
+ keyPlaceholder={getRecordKeyPlaceholder(field)}
248
+ />
249
+ )
250
+ } else if (field.recordKind === 'mcpServers') {
251
+ control = (
252
+ <McpServersRecordEditor
253
+ value={recordValue}
254
+ onChange={handleValueChange}
255
+ t={t}
256
+ keyPlaceholder={getRecordKeyPlaceholder(field)}
257
+ />
258
+ )
259
+ } else if (field.recordKind === 'keyValue') {
260
+ control = (
261
+ <KeyValueEditor
262
+ value={recordValue as Record<string, string>}
263
+ onChange={handleValueChange}
264
+ t={t}
265
+ keyPlaceholder={getRecordKeyPlaceholder(field)}
266
+ />
267
+ )
268
+ } else if (field.recordKind === 'boolean') {
269
+ control = (
270
+ <BooleanRecordEditor
271
+ value={recordValue as Record<string, boolean>}
272
+ onChange={handleValueChange}
273
+ t={t}
274
+ keyPlaceholder={getRecordKeyPlaceholder(field)}
275
+ />
276
+ )
277
+ } else {
278
+ control = (
279
+ <RecordJsonEditor
280
+ value={recordValue}
281
+ onChange={handleValueChange}
282
+ t={t}
283
+ keyPlaceholder={getRecordKeyPlaceholder(field)}
284
+ />
285
+ )
286
+ }
287
+ } else if (field.type === 'shortcut') {
288
+ const isMac = navigator.platform.includes('Mac')
289
+ control = (
290
+ <ShortcutInput
291
+ value={typeof valueToUse === 'string' ? valueToUse : ''}
292
+ onChange={(next) => handleValueChange(next)}
293
+ placeholder={t('config.editor.shortcutPlaceholder')}
294
+ isMac={isMac}
295
+ t={t}
296
+ />
297
+ )
298
+ }
299
+
300
+ if (directRecordSections.has(sectionKey) && field.type === 'record') {
301
+ return (
302
+ <div key={`${field.path.join('.')}-${field.type}-${field.recordKind ?? ''}`}>
303
+ {control}
304
+ </div>
305
+ )
306
+ }
307
+ return (
308
+ <FieldRow
309
+ key={`${field.path.join('.')}-${field.type}-${field.labelKey ?? ''}-${field.recordKind ?? ''}`}
310
+ title={label}
311
+ description={description}
312
+ icon={icon}
313
+ layout={isStacked ? 'stacked' : 'inline'}
314
+ >
315
+ {control}
316
+ </FieldRow>
317
+ )
318
+ }
319
+
320
+ return (
321
+ <div className='config-view__field-stack'>
322
+ {orderedGroups.map((groupKey) => {
323
+ const groupFields = groupedFields[groupKey] ?? []
324
+ const hideEmptyGroups = new Set(['base', 'permissions'])
325
+ if (hideEmptyGroups.has(groupKey)) {
326
+ const hasGroupValues = groupFields.some((field) => {
327
+ const fieldValue = getValueByPath(value, field.path)
328
+ if (typeof fieldValue === 'boolean') return fieldValue
329
+ return !isEmptyValue(fieldValue)
330
+ })
331
+ if (!hasGroupValues) {
332
+ return null
333
+ }
334
+ }
335
+ if (groupKey === 'default') {
336
+ if (directRecordSections.has(sectionKey)) {
337
+ return (
338
+ <div key={groupKey}>
339
+ {groupFields.map(renderField)}
340
+ </div>
341
+ )
342
+ }
343
+ return (
344
+ <div key={groupKey} className='config-view__field-list'>
345
+ {groupFields.map(renderField)}
346
+ </div>
347
+ )
348
+ }
349
+ const groupLabel = (() => {
350
+ const labelKey = configGroupMeta[sectionKey]?.[groupKey]?.labelKey
351
+ if (labelKey) return t(labelKey)
352
+ return groupKey === 'base'
353
+ ? t('config.sectionGroups.base')
354
+ : groupKey === 'permissions'
355
+ ? t('config.sectionGroups.permissions')
356
+ : groupKey === 'env'
357
+ ? t('config.sectionGroups.env')
358
+ : sectionKey === 'plugins'
359
+ ? t('config.sectionGroups.plugins')
360
+ : t('config.sectionGroups.items')
361
+ })()
362
+ const visibleFields = groupFields.filter(field => field.hidden !== true)
363
+ const collapseFields = visibleFields.filter(field => field.collapse != null)
364
+ const nonCollapseFields = visibleFields.filter(field => field.collapse == null)
365
+ const collapseGroups = collapseFields.reduce<
366
+ Map<string, { meta: NonNullable<FieldSpec['collapse']>; fields: FieldSpec[] }>
367
+ >(
368
+ (acc, field) => {
369
+ const meta = field.collapse
370
+ if (!meta) return acc
371
+ const existing = acc.get(meta.key)
372
+ if (existing) {
373
+ existing.fields.push(field)
374
+ } else {
375
+ acc.set(meta.key, { meta, fields: [field] })
376
+ }
377
+ return acc
378
+ },
379
+ new Map()
380
+ )
381
+ const collapseItems = Array.from(collapseGroups.values()).map((group) => ({
382
+ key: group.meta.key,
383
+ collapsible: 'header' as const,
384
+ label: (
385
+ <div className='config-view__collapse-header'>
386
+ <div className='config-view__collapse-header-main'>
387
+ <div className='config-view__collapse-title'>
388
+ {t(group.meta.labelKey)}
389
+ </div>
390
+ {group.meta.descKey && (
391
+ <div className='config-view__collapse-desc'>
392
+ {t(group.meta.descKey)}
393
+ </div>
394
+ )}
395
+ </div>
396
+ {group.meta.togglePath && (
397
+ <Switch
398
+ checked={!getValueByPath(value, group.meta.togglePath)}
399
+ onChange={(next) => {
400
+ const nextValue = setValueByPath(value, group.meta.togglePath!, !next)
401
+ onChange(nextValue)
402
+ }}
403
+ onClick={(_, event) => {
404
+ event.stopPropagation()
405
+ }}
406
+ />
407
+ )}
408
+ </div>
409
+ ),
410
+ children: (
411
+ <div className='config-view__field-list'>
412
+ {group.fields.map(renderField)}
413
+ </div>
414
+ )
415
+ }))
416
+ return (
417
+ <div key={groupKey} className='config-view__subsection'>
418
+ <div className='config-view__subsection-title'>
419
+ {groupLabel}
420
+ </div>
421
+ <div className='config-view__subsection-body'>
422
+ {nonCollapseFields.map(renderField)}
423
+ {collapseItems.length > 0 && (
424
+ <Collapse
425
+ className='config-view__collapse-group config-view__field-row'
426
+ ghost
427
+ items={collapseItems}
428
+ />
429
+ )}
430
+ </div>
431
+ </div>
432
+ )
433
+ })}
434
+ </div>
435
+ )
436
+ }
@@ -0,0 +1,67 @@
1
+ import '../ConfigView.scss'
2
+
3
+ import type { ReactNode } from 'react'
4
+
5
+ import { SectionForm } from './ConfigSectionForm'
6
+ import type { TranslationFn } from './configUtils'
7
+ import type { FieldSpec } from './configSchema'
8
+
9
+ export function ConfigSectionPanel({
10
+ sectionKey,
11
+ title,
12
+ icon,
13
+ fields,
14
+ value,
15
+ onChange,
16
+ mergedModelServices,
17
+ mergedAdapters,
18
+ selectedModelService,
19
+ headerExtra,
20
+ t,
21
+ className
22
+ }: {
23
+ sectionKey: string
24
+ title: ReactNode
25
+ icon?: ReactNode
26
+ fields?: FieldSpec[]
27
+ value: unknown
28
+ onChange: (nextValue: unknown) => void
29
+ mergedModelServices: Record<string, unknown>
30
+ mergedAdapters: Record<string, unknown>
31
+ selectedModelService?: string
32
+ headerExtra?: ReactNode
33
+ t: TranslationFn
34
+ className?: string
35
+ }) {
36
+ const wrapClassName = ['config-view__editor-wrap', className].filter(Boolean).join(' ')
37
+ const hasHeader = title != null || icon != null || headerExtra != null
38
+ return (
39
+ <div className={wrapClassName}>
40
+ {hasHeader && (
41
+ <div className='config-view__section-header'>
42
+ <div className='config-view__section-title'>
43
+ {icon != null && (
44
+ <span className='material-symbols-rounded config-view__section-icon'>
45
+ {icon}
46
+ </span>
47
+ )}
48
+ <span>{title}</span>
49
+ </div>
50
+ {headerExtra}
51
+ </div>
52
+ )}
53
+ <div className='config-view__card'>
54
+ <SectionForm
55
+ sectionKey={sectionKey}
56
+ fields={fields}
57
+ value={value}
58
+ onChange={onChange}
59
+ mergedModelServices={mergedModelServices}
60
+ mergedAdapters={mergedAdapters}
61
+ selectedModelService={selectedModelService}
62
+ t={t}
63
+ />
64
+ </div>
65
+ </div>
66
+ )
67
+ }
@@ -0,0 +1,11 @@
1
+ .config-shortcut-input {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 8px;
5
+ }
6
+
7
+ .config-shortcut-input .ant-input {
8
+ text-align: center;
9
+ font-weight: 600;
10
+ letter-spacing: 0.5px;
11
+ }
@@ -0,0 +1,52 @@
1
+ import './ConfigShortcutInput.scss'
2
+
3
+ import { Button, Input, Tooltip } from 'antd'
4
+ import React from 'react'
5
+
6
+ import { formatShortcutLabel, getShortcutFromEvent } from '../../utils/shortcutUtils'
7
+
8
+ export const ShortcutInput = ({
9
+ value,
10
+ onChange,
11
+ placeholder,
12
+ isMac,
13
+ t
14
+ }: {
15
+ value: string
16
+ onChange: (nextValue: string) => void
17
+ placeholder: string
18
+ isMac: boolean
19
+ t: (key: string) => string
20
+ }) => {
21
+ const label = value.trim() === '' ? '' : formatShortcutLabel(value, isMac)
22
+ return (
23
+ <div className='config-shortcut-input'>
24
+ <Input
25
+ value={label}
26
+ placeholder={placeholder}
27
+ readOnly
28
+ onKeyDown={(event) => {
29
+ if (event.key === 'Backspace' || event.key === 'Delete') {
30
+ event.preventDefault()
31
+ onChange('')
32
+ return
33
+ }
34
+ const nextShortcut = getShortcutFromEvent(event)
35
+ if (nextShortcut == null) return
36
+ event.preventDefault()
37
+ onChange(nextShortcut)
38
+ }}
39
+ />
40
+ <Tooltip title={t('config.editor.clearShortcut')}>
41
+ <Button
42
+ size='small'
43
+ type='text'
44
+ className='config-view__icon-button config-view__icon-button--compact'
45
+ aria-label={t('config.editor.clearShortcut')}
46
+ icon={<span className='material-symbols-rounded'>close</span>}
47
+ onClick={() => onChange('')}
48
+ />
49
+ </Tooltip>
50
+ </div>
51
+ )
52
+ }
@@ -0,0 +1,57 @@
1
+ import { Radio } from 'antd'
2
+
3
+ import type { ConfigSource } from '@vibe-forge/core'
4
+
5
+ import type { TranslationFn } from './configUtils'
6
+
7
+ export function ConfigSourceSwitch({
8
+ value,
9
+ onChange,
10
+ configPresent,
11
+ t
12
+ }: {
13
+ value: ConfigSource
14
+ onChange: (value: ConfigSource) => void
15
+ configPresent?: { project?: boolean; user?: boolean }
16
+ t: TranslationFn
17
+ }) {
18
+ return (
19
+ <Radio.Group
20
+ value={value}
21
+ optionType='button'
22
+ buttonStyle='solid'
23
+ size='small'
24
+ onChange={(event) => {
25
+ onChange(event.target.value as ConfigSource)
26
+ }}
27
+ options={[
28
+ {
29
+ label: (
30
+ <span className='config-view__source-option'>
31
+ <span className='material-symbols-rounded'>folder</span>
32
+ <span>
33
+ {configPresent?.project === true
34
+ ? t('config.sources.project')
35
+ : t('config.sources.projectMissing')}
36
+ </span>
37
+ </span>
38
+ ),
39
+ value: 'project'
40
+ },
41
+ {
42
+ label: (
43
+ <span className='config-view__source-option'>
44
+ <span className='material-symbols-rounded'>person</span>
45
+ <span>
46
+ {configPresent?.user === true
47
+ ? t('config.sources.user')
48
+ : t('config.sources.userMissing')}
49
+ </span>
50
+ </span>
51
+ ),
52
+ value: 'user'
53
+ }
54
+ ]}
55
+ />
56
+ )
57
+ }