@vibe-forge/client 0.3.0 → 0.4.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 (158) hide show
  1. package/cli.cjs +1 -1
  2. package/dist/assets/{arc-CwMXUVsq.js → arc-DgIxeTMg.js} +1 -1
  3. package/dist/assets/{blockDiagram-c4efeb88-CGxJV7KJ.js → blockDiagram-c4efeb88-CEAob3X9.js} +1 -1
  4. package/dist/assets/{c4Diagram-c83219d4-BKhin7cY.js → c4Diagram-c83219d4-DwIxpDKd.js} +1 -1
  5. package/dist/assets/channel-DhtnrNJ6.js +1 -0
  6. package/dist/assets/{classDiagram-beda092f-BASmn22R.js → classDiagram-beda092f-Cz1q8u_0.js} +1 -1
  7. package/dist/assets/{classDiagram-v2-2358418a-BUk9rNBX.js → classDiagram-v2-2358418a-CImgTuwd.js} +1 -1
  8. package/dist/assets/clone-7bHB6YkC.js +1 -0
  9. package/dist/assets/{createText-1719965b-2XqnWjQY.js → createText-1719965b-C1_HJcCc.js} +1 -1
  10. package/dist/assets/devicon-BWlTeAUU.woff +0 -0
  11. package/dist/assets/devicon-CirD-cQx.ttf +0 -0
  12. package/dist/assets/devicon-Dg8iWy0i.svg +1211 -0
  13. package/dist/assets/devicon-TqfHp33-.eot +0 -0
  14. package/dist/assets/{edges-96097737-B7e32Jeg.js → edges-96097737-BU8qStzd.js} +1 -1
  15. package/dist/assets/{erDiagram-0228fc6a-CCR2or72.js → erDiagram-0228fc6a-DNA1Fz2L.js} +1 -1
  16. package/dist/assets/{flowDb-c6c81e3f-B72HWT9x.js → flowDb-c6c81e3f-DjiCStMN.js} +1 -1
  17. package/dist/assets/{flowDiagram-50d868cf-WOi0KARY.js → flowDiagram-50d868cf-CSDi0-RD.js} +1 -1
  18. package/dist/assets/flowDiagram-v2-4f6560a1-_13Sz5Wh.js +1 -0
  19. package/dist/assets/{flowchart-elk-definition-6af322e1-i_Yd0LCE.js → flowchart-elk-definition-6af322e1-DrhIMas7.js} +1 -1
  20. package/dist/assets/{ganttDiagram-a2739b55-CFH9zF14.js → ganttDiagram-a2739b55-CTZnUP5z.js} +1 -1
  21. package/dist/assets/{gitGraphDiagram-82fe8481-DglKfMze.js → gitGraphDiagram-82fe8481-COOW7jTi.js} +1 -1
  22. package/dist/assets/{graph-BKbBNGPf.js → graph-CIkpD4Kx.js} +1 -1
  23. package/dist/assets/{index-5325376f-BK7F9nSl.js → index-5325376f-aVVRRTIu.js} +1 -1
  24. package/dist/assets/index-D1giUI7r.css +1 -0
  25. package/dist/assets/index-DRSI_ZIL.js +514 -0
  26. package/dist/assets/{infoDiagram-8eee0895-BLFL77_D.js → infoDiagram-8eee0895-DQpZ1LVD.js} +1 -1
  27. package/dist/assets/{journeyDiagram-c64418c1-CS9XctDL.js → journeyDiagram-c64418c1-DoKguIuk.js} +1 -1
  28. package/dist/assets/{layout-By3JZZGt.js → layout-Tnmha8Nh.js} +1 -1
  29. package/dist/assets/{line-9GUsXbwv.js → line-BQR2SOyl.js} +1 -1
  30. package/dist/assets/{linear-DzGV4E9N.js → linear-DlG0eemV.js} +1 -1
  31. package/dist/assets/{mermaid.core-CG3Ib42Q.js → mermaid.core-BnwYO0He.js} +6 -6
  32. package/dist/assets/{mindmap-definition-8da855dc-WQ3LPKJU.js → mindmap-definition-8da855dc-BllYwDID.js} +1 -1
  33. package/dist/assets/{pieDiagram-a8764435-DHVIUZiN.js → pieDiagram-a8764435-DwCkhPVc.js} +1 -1
  34. package/dist/assets/{quadrantDiagram-1e28029f-C3G9Ye8-.js → quadrantDiagram-1e28029f-c40GKTU0.js} +1 -1
  35. package/dist/assets/{requirementDiagram-08caed73-C9ES1D5G.js → requirementDiagram-08caed73-DnQp2Tk6.js} +1 -1
  36. package/dist/assets/{sankeyDiagram-a04cb91d-B4BKXclQ.js → sankeyDiagram-a04cb91d-CnJrs13b.js} +1 -1
  37. package/dist/assets/{sequenceDiagram-c5b8d532-DrgEb25G.js → sequenceDiagram-c5b8d532-1YBwnpKu.js} +1 -1
  38. package/dist/assets/{stateDiagram-1ecb1508-CF1XWARJ.js → stateDiagram-1ecb1508-BFBxQ6Fh.js} +1 -1
  39. package/dist/assets/{stateDiagram-v2-c2b004d7-IO3i3yXv.js → stateDiagram-v2-c2b004d7-Dmechvv2.js} +1 -1
  40. package/dist/assets/{styles-b4e223ce-DACN9aSc.js → styles-b4e223ce-DWWfWX8O.js} +1 -1
  41. package/dist/assets/{styles-ca3715f6-bekm2WLP.js → styles-ca3715f6-CKKvZxaU.js} +1 -1
  42. package/dist/assets/{styles-d45a18b0-OzTDVBb8.js → styles-d45a18b0-dKMOUh9p.js} +1 -1
  43. package/dist/assets/{svgDrawCommon-b86b1483-BWroJerr.js → svgDrawCommon-b86b1483-CBgjChPM.js} +1 -1
  44. package/dist/assets/{timeline-definition-faaaa080-CCfRNigO.js → timeline-definition-faaaa080-NCt-HHmb.js} +1 -1
  45. package/dist/assets/{xychartDiagram-f5964ef8-C3cbfVqN.js → xychartDiagram-f5964ef8-BJhXS4dG.js} +1 -1
  46. package/dist/index.html +2 -7
  47. package/index.html +0 -5
  48. package/package.json +11 -6
  49. package/src/App.tsx +2 -0
  50. package/src/api/README.md +26 -0
  51. package/src/api/automation.ts +88 -0
  52. package/src/api/base.ts +54 -0
  53. package/src/api/benchmark.ts +45 -0
  54. package/src/api/config.ts +24 -0
  55. package/src/api/knowledge.ts +72 -0
  56. package/src/api/projects.ts +15 -0
  57. package/src/api/sessions.ts +82 -0
  58. package/src/api/types.ts +20 -0
  59. package/src/api.ts +44 -269
  60. package/src/components/AutomationView/AutomationView.scss +5 -1
  61. package/src/components/AutomationView/RuleFormPanel.tsx +3 -2
  62. package/src/components/AutomationView/TaskList.scss +4 -6
  63. package/src/components/AutomationView/TaskList.tsx +2 -1
  64. package/src/components/AutomationView/TriggerList.scss +4 -1
  65. package/src/components/BenchmarkView/BenchmarkCasePanel.scss +267 -0
  66. package/src/components/BenchmarkView/BenchmarkCasePanel.tsx +309 -0
  67. package/src/components/BenchmarkView/BenchmarkSidebar.scss +182 -0
  68. package/src/components/BenchmarkView/BenchmarkSidebar.tsx +262 -0
  69. package/src/components/BenchmarkView/BenchmarkView.scss +78 -0
  70. package/src/components/BenchmarkView/index.tsx +197 -0
  71. package/src/components/BenchmarkView/types.ts +10 -0
  72. package/src/components/BenchmarkView/utils.ts +21 -0
  73. package/src/components/Chat.tsx +37 -29
  74. package/src/components/{chat/CodeBlock.tsx → CodeBlock.tsx} +3 -1
  75. package/src/components/ConfigView.tsx +7 -0
  76. package/src/components/{chat/MarkdownContent.tsx → MarkdownContent.tsx} +1 -1
  77. package/src/components/NavRail.tsx +7 -0
  78. package/src/components/chat/ChatHeader.scss +37 -19
  79. package/src/components/chat/ChatHeader.tsx +6 -9
  80. package/src/components/chat/ChatHistoryView.tsx +89 -45
  81. package/src/components/chat/CurrentTodoList.tsx +10 -9
  82. package/src/components/chat/{MessageItem.scss → Messages/MessageItem.scss} +14 -0
  83. package/src/components/chat/{MessageItem.tsx → Messages/MessageItem.tsx} +30 -8
  84. package/src/components/chat/{messageUtils.ts → Messages/message-utils.ts} +1 -1
  85. package/src/components/chat/{Sender.scss → Sender/Sender.scss} +80 -0
  86. package/src/components/chat/{Sender.tsx → Sender/Sender.tsx} +161 -5
  87. package/src/components/chat/tools/DefaultTool.tsx +184 -21
  88. package/src/components/chat/tools/adapter-claude/BashTool.scss +67 -51
  89. package/src/components/chat/tools/adapter-claude/BashTool.tsx +83 -49
  90. package/src/components/chat/tools/adapter-claude/GlobTool.scss +0 -79
  91. package/src/components/chat/tools/adapter-claude/GlobTool.tsx +16 -36
  92. package/src/components/chat/tools/adapter-claude/GrepTool.scss +0 -87
  93. package/src/components/chat/tools/adapter-claude/GrepTool.tsx +22 -41
  94. package/src/components/chat/tools/adapter-claude/LSTool.scss +0 -79
  95. package/src/components/chat/tools/adapter-claude/LSTool.tsx +15 -15
  96. package/src/components/chat/tools/adapter-claude/ReadTool.scss +0 -55
  97. package/src/components/chat/tools/adapter-claude/ReadTool.tsx +20 -42
  98. package/src/components/chat/tools/adapter-claude/TodoTool.scss +8 -23
  99. package/src/components/chat/tools/adapter-claude/TodoTool.tsx +24 -11
  100. package/src/components/chat/tools/adapter-claude/WriteTool.scss +21 -69
  101. package/src/components/chat/tools/adapter-claude/WriteTool.tsx +22 -58
  102. package/src/components/chat/tools/adapter-claude/index.ts +4 -10
  103. package/src/components/chat/tools/adapter-claude/utils.ts +54 -0
  104. package/src/components/chat/tools/core/ToolCallBox.scss +356 -0
  105. package/src/components/chat/{ToolGroup.tsx → tools/core/ToolGroup.tsx} +26 -7
  106. package/src/components/chat/{ToolRenderer.tsx → tools/core/ToolRenderer.tsx} +6 -4
  107. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.scss +11 -0
  108. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +75 -0
  109. package/src/components/chat/tools/plugin-chrome-devtools/index.ts +45 -0
  110. package/src/components/chat/tools/task/GetTaskInfoTool.scss +2 -27
  111. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +48 -38
  112. package/src/components/chat/tools/task/ListTasksTool.scss +3 -28
  113. package/src/components/chat/tools/task/ListTasksTool.tsx +11 -8
  114. package/src/components/chat/tools/task/StartTasksTool.scss +3 -28
  115. package/src/components/chat/tools/task/StartTasksTool.tsx +14 -17
  116. package/src/components/chat/tools/task/components/TaskRow.scss +105 -0
  117. package/src/components/chat/tools/task/components/TaskRow.tsx +163 -0
  118. package/src/components/chat/tools/task/components/TaskToolCard.scss +15 -15
  119. package/src/components/chat/tools/task/components/TaskToolCard.tsx +8 -6
  120. package/src/components/config/ConfigSectionForm.tsx +12 -1
  121. package/src/components/config/channelDefinitions.ts +6 -0
  122. package/src/components/config/configSchema.ts +10 -1
  123. package/src/components/config/recordEditors/ChannelRecordEditor.scss +1 -0
  124. package/src/components/config/recordEditors/ChannelRecordEditor.tsx +397 -0
  125. package/src/components/config/recordEditors/index.tsx +1 -0
  126. package/src/components/knowledge-base/components/RuleItem.tsx +1 -1
  127. package/src/components/knowledge-base/components/SpecItem.tsx +1 -1
  128. package/src/hooks/chat/use-chat-interaction.ts +26 -0
  129. package/src/{components/chat/useChatModels.tsx → hooks/chat/use-chat-models.tsx} +46 -15
  130. package/src/hooks/chat/use-chat-permission-mode.ts +47 -0
  131. package/src/hooks/chat/use-chat-scroll.ts +51 -0
  132. package/src/hooks/chat/use-chat-session-actions.ts +147 -0
  133. package/src/hooks/chat/use-chat-session-messages.ts +250 -0
  134. package/src/hooks/chat/use-chat-session.ts +57 -0
  135. package/src/hooks/chat/use-chat-view.ts +39 -0
  136. package/src/main.tsx +10 -13
  137. package/src/resources/locales/en.json +66 -0
  138. package/src/resources/locales/zh.json +66 -0
  139. package/src/runtime-config.ts +52 -0
  140. package/src/vite-env.d.ts +11 -0
  141. package/src/ws.ts +5 -3
  142. package/vite.config.ts +12 -4
  143. package/dist/assets/channel-jbCEHqbG.js +0 -1
  144. package/dist/assets/clone-CCRKqS4L.js +0 -1
  145. package/dist/assets/flowDiagram-v2-4f6560a1-Baslbgn4.js +0 -1
  146. package/dist/assets/index-B0qfCb1G.css +0 -1
  147. package/dist/assets/index-CNo75dYr.js +0 -497
  148. package/src/components/chat/ToolCallBox.scss +0 -137
  149. package/src/components/chat/useChatSession.ts +0 -370
  150. /package/src/components/{chat/CodeBlock.scss → CodeBlock.scss} +0 -0
  151. /package/src/components/chat/{MessageFooter.tsx → Messages/MessageFooter.tsx} +0 -0
  152. /package/src/components/chat/{CompletionMenu.scss → Sender/CompletionMenu.scss} +0 -0
  153. /package/src/components/chat/{CompletionMenu.tsx → Sender/CompletionMenu.tsx} +0 -0
  154. /package/src/components/chat/{ThinkingStatus.scss → Sender/ThinkingStatus.scss} +0 -0
  155. /package/src/components/chat/{ThinkingStatus.tsx → Sender/ThinkingStatus.tsx} +0 -0
  156. /package/src/components/chat/{ToolCallBox.tsx → tools/core/ToolCallBox.tsx} +0 -0
  157. /package/src/components/chat/{ToolGroup.scss → tools/core/ToolGroup.scss} +0 -0
  158. /package/src/{components/chat/safeSerialize.ts → utils/safe-serialize.ts} +0 -0
@@ -0,0 +1,397 @@
1
+ import './ChannelRecordEditor.scss'
2
+
3
+ import { Button, Input, InputNumber, Select, Switch, Tooltip } from 'antd'
4
+ import { useEffect, useMemo, useState } from 'react'
5
+ import type { ZodRawShape, ZodTypeAny } from 'zod'
6
+ import { z } from 'zod'
7
+
8
+ import type { ChannelDescriptor } from '@vibe-forge/core/channel'
9
+ import { channelBaseSchema } from '@vibe-forge/core/channel'
10
+
11
+ import { ComplexTextEditor, StringArrayEditor } from '../ConfigEditors'
12
+ import { FieldRow } from '../ConfigFieldRow'
13
+ import { channelDefinitions } from '../channelDefinitions'
14
+ import { getTypeIcon, isSensitiveKey } from '../configUtils'
15
+ import type { TranslationFn } from '../configUtils'
16
+
17
+ type ChannelRecordValue = Record<string, unknown>
18
+
19
+ const getObjectShape = (schema: ZodTypeAny): ZodRawShape => {
20
+ if (schema instanceof z.ZodObject) return schema.shape
21
+ if (schema instanceof z.ZodEffects) return getObjectShape(schema.innerType())
22
+ if (schema instanceof z.ZodOptional) return getObjectShape(schema.unwrap())
23
+ if (schema instanceof z.ZodDefault) return getObjectShape(schema.removeDefault())
24
+ if (schema instanceof z.ZodNullable) return getObjectShape(schema.unwrap())
25
+ return {}
26
+ }
27
+
28
+ const unwrapSchema = (schema: ZodTypeAny): ZodTypeAny => {
29
+ if (schema instanceof z.ZodEffects) return unwrapSchema(schema.innerType())
30
+ if (schema instanceof z.ZodOptional) return unwrapSchema(schema.unwrap())
31
+ if (schema instanceof z.ZodDefault) return unwrapSchema(schema.removeDefault())
32
+ if (schema instanceof z.ZodNullable) return unwrapSchema(schema.unwrap())
33
+ return schema
34
+ }
35
+
36
+ const getDefaultValue = (schema: ZodTypeAny): unknown => {
37
+ if (schema instanceof z.ZodDefault) return schema._def.defaultValue()
38
+ if (schema instanceof z.ZodOptional) return undefined
39
+ if (schema instanceof z.ZodNullable) return null
40
+ if (schema instanceof z.ZodLiteral) return schema.value
41
+ if (schema instanceof z.ZodEnum) return schema.options[0]
42
+ if (schema instanceof z.ZodNativeEnum) {
43
+ const values = Object.values(schema.enum)
44
+ return values.length > 0 ? values[0] : undefined
45
+ }
46
+ if (schema instanceof z.ZodString) return ''
47
+ if (schema instanceof z.ZodNumber) return 0
48
+ if (schema instanceof z.ZodBoolean) return false
49
+ if (schema instanceof z.ZodArray) return []
50
+ if (schema instanceof z.ZodObject) {
51
+ const shape = getObjectShape(schema)
52
+ return Object.fromEntries(Object.entries(shape).map(([key, value]) => [key, getDefaultValue(value)]))
53
+ }
54
+ if (schema instanceof z.ZodRecord) return {}
55
+ if (schema instanceof z.ZodEffects) return getDefaultValue(schema.innerType())
56
+ return undefined
57
+ }
58
+
59
+ const toLabel = (key: string) =>
60
+ key
61
+ .replace(/_/g, ' ')
62
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
63
+ .replace(/\b\w/g, char => char.toUpperCase())
64
+
65
+ const buildRecordDefaults = (definition: ChannelDescriptor) => {
66
+ const shape = getObjectShape(definition.configSchema)
67
+ const entries = Object.entries(shape)
68
+ return entries.reduce<Record<string, unknown>>((acc, [key, schema]) => {
69
+ acc[key] = getDefaultValue(schema)
70
+ return acc
71
+ }, {})
72
+ }
73
+
74
+ const getChannelDefinition = (type: string | undefined) =>
75
+ channelDefinitions.find(definition => definition.type === type)
76
+
77
+ export const ChannelRecordEditor = ({
78
+ value,
79
+ onChange,
80
+ t,
81
+ keyPlaceholder
82
+ }: {
83
+ value: Record<string, unknown>
84
+ onChange: (nextValue: Record<string, unknown>) => void
85
+ t: TranslationFn
86
+ keyPlaceholder: string
87
+ }) => {
88
+ const [newKey, setNewKey] = useState('')
89
+ const [newType, setNewType] = useState(channelDefinitions[0]?.type ?? '')
90
+ const entries = useMemo(() => Object.entries(value), [value])
91
+ const [collapsedKeys, setCollapsedKeys] = useState<Record<string, boolean>>(() => (
92
+ Object.fromEntries(entries.map(([key]) => [key, true]))
93
+ ))
94
+ const baseKeys = useMemo(() => new Set(Object.keys(getObjectShape(channelBaseSchema))), [])
95
+ const typeOptions = useMemo(() =>
96
+ channelDefinitions.map(definition => ({
97
+ value: definition.type,
98
+ label: definition.label
99
+ })), [])
100
+
101
+ useEffect(() => {
102
+ setCollapsedKeys(prev => {
103
+ const next: Record<string, boolean> = {}
104
+ for (const [key] of entries) {
105
+ next[key] = prev[key] ?? true
106
+ }
107
+ return next
108
+ })
109
+ }, [entries])
110
+
111
+ const updateChannel = (key: string, nextValue: ChannelRecordValue) => {
112
+ onChange({ ...value, [key]: nextValue })
113
+ }
114
+
115
+ const renderField = (
116
+ channelKey: string,
117
+ recordValue: ChannelRecordValue,
118
+ fieldKey: string,
119
+ fieldSchema: ZodTypeAny
120
+ ) => {
121
+ const rawSchema = unwrapSchema(fieldSchema)
122
+ const description = rawSchema.description ?? ''
123
+ const label = toLabel(fieldKey)
124
+ const currentValue = recordValue[fieldKey]
125
+ const valueToUse = currentValue !== undefined ? currentValue : getDefaultValue(fieldSchema)
126
+
127
+ if (rawSchema instanceof z.ZodString) {
128
+ const isSensitive = isSensitiveKey(fieldKey)
129
+ return (
130
+ <FieldRow key={fieldKey} title={label} description={description} icon={getTypeIcon('string')}>
131
+ {isSensitive
132
+ ? (
133
+ <Input.Password
134
+ value={typeof valueToUse === 'string' ? valueToUse : ''}
135
+ onChange={(event) => {
136
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: event.target.value })
137
+ }}
138
+ placeholder={t('config.editor.secretPlaceholder')}
139
+ />
140
+ )
141
+ : (
142
+ <Input
143
+ value={typeof valueToUse === 'string' ? valueToUse : ''}
144
+ onChange={(event) => {
145
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: event.target.value })
146
+ }}
147
+ />
148
+ )}
149
+ </FieldRow>
150
+ )
151
+ }
152
+
153
+ if (rawSchema instanceof z.ZodNumber) {
154
+ return (
155
+ <FieldRow key={fieldKey} title={label} description={description} icon={getTypeIcon('number')}>
156
+ <InputNumber
157
+ value={typeof valueToUse === 'number' ? valueToUse : 0}
158
+ onChange={(nextValue) => {
159
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: nextValue ?? 0 })
160
+ }}
161
+ />
162
+ </FieldRow>
163
+ )
164
+ }
165
+
166
+ if (rawSchema instanceof z.ZodBoolean) {
167
+ return (
168
+ <FieldRow key={fieldKey} title={label} description={description} icon={getTypeIcon('boolean')}>
169
+ <Switch
170
+ checked={Boolean(valueToUse)}
171
+ onChange={(checked) => {
172
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: checked })
173
+ }}
174
+ />
175
+ </FieldRow>
176
+ )
177
+ }
178
+
179
+ if (rawSchema instanceof z.ZodEnum || rawSchema instanceof z.ZodNativeEnum) {
180
+ const optionValues: string[] = (
181
+ rawSchema instanceof z.ZodEnum
182
+ ? rawSchema.options
183
+ : Object.values(rawSchema.enum)
184
+ ).map((option: string | number) => String(option))
185
+ return (
186
+ <FieldRow key={fieldKey} title={label} description={description} icon={getTypeIcon('string')}>
187
+ <Select
188
+ value={typeof valueToUse === 'string' ? valueToUse : undefined}
189
+ options={optionValues.map((option) => ({ value: option, label: option }))}
190
+ onChange={(nextValue) => {
191
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: nextValue })
192
+ }}
193
+ />
194
+ </FieldRow>
195
+ )
196
+ }
197
+
198
+ if (rawSchema instanceof z.ZodArray) {
199
+ const inner = unwrapSchema(rawSchema.element)
200
+ if (inner instanceof z.ZodString) {
201
+ const items = Array.isArray(valueToUse)
202
+ ? valueToUse.filter(item => typeof item === 'string')
203
+ : []
204
+ return (
205
+ <FieldRow
206
+ key={fieldKey}
207
+ title={label}
208
+ description={description}
209
+ icon={getTypeIcon('array')}
210
+ layout='stacked'
211
+ >
212
+ <StringArrayEditor
213
+ value={items}
214
+ onChange={(nextValue) => {
215
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: nextValue })
216
+ }}
217
+ t={t}
218
+ />
219
+ </FieldRow>
220
+ )
221
+ }
222
+ return (
223
+ <FieldRow
224
+ key={fieldKey}
225
+ title={label}
226
+ description={description}
227
+ icon={getTypeIcon('array')}
228
+ layout='stacked'
229
+ >
230
+ <ComplexTextEditor
231
+ value={Array.isArray(valueToUse) ? valueToUse : []}
232
+ onChange={(nextValue) => {
233
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: nextValue })
234
+ }}
235
+ />
236
+ </FieldRow>
237
+ )
238
+ }
239
+
240
+ return (
241
+ <FieldRow
242
+ key={fieldKey}
243
+ title={label}
244
+ description={description}
245
+ icon={getTypeIcon('object')}
246
+ layout='stacked'
247
+ >
248
+ <ComplexTextEditor
249
+ value={valueToUse ?? {}}
250
+ onChange={(nextValue) => {
251
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: nextValue })
252
+ }}
253
+ />
254
+ </FieldRow>
255
+ )
256
+ }
257
+
258
+ return (
259
+ <div className='config-view__record-list'>
260
+ {entries.map(([key, itemValue]) => {
261
+ const recordValue = (itemValue != null && typeof itemValue === 'object')
262
+ ? itemValue as ChannelRecordValue
263
+ : {}
264
+ const type = typeof recordValue.type === 'string' ? recordValue.type : undefined
265
+ const definition = getChannelDefinition(type)
266
+ const titleValue = typeof recordValue.title === 'string' ? recordValue.title : ''
267
+ const descriptionValue = typeof recordValue.description === 'string' ? recordValue.description : ''
268
+ const displayName = titleValue.trim() !== '' ? titleValue : key
269
+ const typeLabel = definition?.label ?? type ?? t('config.editor.unknownChannelType')
270
+ const isCollapsed = collapsedKeys[key] === true
271
+ const shape = definition ? getObjectShape(definition.configSchema) : {}
272
+ const fieldEntries = Object.entries(shape)
273
+ .filter(([fieldKey]) => fieldKey !== 'type')
274
+ .sort(([a], [b]) => {
275
+ const order = ['title', 'description', 'enabled', 'admins']
276
+ const aIndex = order.indexOf(a)
277
+ const bIndex = order.indexOf(b)
278
+ if (aIndex === -1 && bIndex === -1) return a.localeCompare(b)
279
+ if (aIndex === -1) return 1
280
+ if (bIndex === -1) return -1
281
+ return aIndex - bIndex
282
+ })
283
+
284
+ return (
285
+ <div
286
+ key={key}
287
+ className={`config-view__record-card${isCollapsed ? ' config-view__record-card--collapsed' : ''}`}
288
+ >
289
+ <div className='config-view__record-title'>
290
+ <div className='config-view__record-title-left'>
291
+ <Tooltip title={isCollapsed ? t('config.editor.expand') : t('config.editor.collapse')}>
292
+ <Button
293
+ size='small'
294
+ type='text'
295
+ className='config-view__icon-button config-view__icon-button--compact'
296
+ aria-label={isCollapsed ? t('config.editor.expand') : t('config.editor.collapse')}
297
+ icon={
298
+ <span className='material-symbols-rounded'>{isCollapsed ? 'chevron_right' : 'expand_more'}</span>
299
+ }
300
+ onClick={() => {
301
+ setCollapsedKeys(prev => ({ ...prev, [key]: !isCollapsed }))
302
+ }}
303
+ />
304
+ </Tooltip>
305
+ <div className='config-view__record-heading'>
306
+ <div>{displayName}</div>
307
+ <div className='config-view__record-subtitle'>
308
+ {key} · {typeLabel}
309
+ </div>
310
+ {descriptionValue !== '' && (
311
+ <div className='config-view__record-desc'>{descriptionValue}</div>
312
+ )}
313
+ </div>
314
+ </div>
315
+ <div className='config-view__record-actions'>
316
+ <Tooltip title={t('config.editor.remove')}>
317
+ <Button
318
+ size='small'
319
+ type='text'
320
+ danger
321
+ className='config-view__icon-button config-view__icon-button--compact'
322
+ aria-label={t('config.editor.remove')}
323
+ icon={<span className='material-symbols-rounded'>delete</span>}
324
+ onClick={() => {
325
+ const updated = { ...value }
326
+ delete updated[key]
327
+ onChange(updated)
328
+ }}
329
+ />
330
+ </Tooltip>
331
+ </div>
332
+ </div>
333
+ <div className='config-view__record-body'>
334
+ <div className='config-view__record-fields'>
335
+ <FieldRow
336
+ title={t('config.editor.channelType')}
337
+ description={definition?.description ?? ''}
338
+ icon={getTypeIcon('string')}
339
+ >
340
+ <Select
341
+ value={type}
342
+ options={typeOptions}
343
+ onChange={(nextType) => {
344
+ const nextDefinition = getChannelDefinition(nextType)
345
+ if (!nextDefinition) return
346
+ const defaults = buildRecordDefaults(nextDefinition)
347
+ const preserved = Object.fromEntries(
348
+ Object.entries(recordValue).filter(([fieldKey]) => baseKeys.has(fieldKey))
349
+ )
350
+ updateChannel(key, { ...defaults, ...preserved, type: nextType })
351
+ }}
352
+ />
353
+ </FieldRow>
354
+ {fieldEntries.map(([fieldKey, fieldSchema]) => (
355
+ renderField(key, recordValue, fieldKey, fieldSchema)
356
+ ))}
357
+ </div>
358
+ </div>
359
+ </div>
360
+ )
361
+ })}
362
+ <div className='config-view__record-add'>
363
+ <div className='config-view__record-add-inputs'>
364
+ <Input
365
+ value={newKey}
366
+ placeholder={keyPlaceholder}
367
+ onChange={(event) => setNewKey(event.target.value)}
368
+ />
369
+ <Select
370
+ value={newType}
371
+ options={typeOptions}
372
+ onChange={(nextValue) => setNewType(nextValue)}
373
+ />
374
+ <Tooltip title={t('common.confirm')}>
375
+ <Button
376
+ size='small'
377
+ type='primary'
378
+ className='config-view__icon-button'
379
+ aria-label={t('common.confirm')}
380
+ icon={<span className='material-symbols-rounded'>check</span>}
381
+ disabled={newKey.trim() === '' || Object.hasOwn(value, newKey) || newType === ''}
382
+ onClick={() => {
383
+ const definition = getChannelDefinition(newType)
384
+ if (!definition) return
385
+ onChange({
386
+ ...value,
387
+ [newKey]: buildRecordDefaults(definition)
388
+ })
389
+ setNewKey('')
390
+ }}
391
+ />
392
+ </Tooltip>
393
+ </div>
394
+ </div>
395
+ </div>
396
+ )
397
+ }
@@ -1,3 +1,4 @@
1
+ export { ChannelRecordEditor } from './ChannelRecordEditor'
1
2
  export { BooleanRecordEditor } from './BooleanRecordEditor'
2
3
  export { KeyValueEditor } from './KeyValueEditor'
3
4
  export { McpServersRecordEditor } from './McpServersRecordEditor'
@@ -5,7 +5,7 @@ import useSWR from 'swr'
5
5
 
6
6
  import type { RuleDetail, RuleSummary } from '#~/api.js'
7
7
  import { getRuleDetail } from '#~/api.js'
8
- import { MarkdownContent } from '#~/components/chat/MarkdownContent'
8
+ import { MarkdownContent } from '#~/components/MarkdownContent'
9
9
  import { LoadingState } from './LoadingState'
10
10
 
11
11
  type RuleItemProps = {
@@ -7,7 +7,7 @@ import useSWR from 'swr'
7
7
 
8
8
  import type { SpecDetail, SpecSummary } from '#~/api.js'
9
9
  import { getSpecDetail } from '#~/api.js'
10
- import { MarkdownContent } from '#~/components/chat/MarkdownContent'
10
+ import { MarkdownContent } from '#~/components/MarkdownContent'
11
11
  import { LoadingState } from './LoadingState'
12
12
  import { MetaList } from './MetaList'
13
13
 
@@ -0,0 +1,26 @@
1
+ import { useCallback, useState } from 'react'
2
+
3
+ import type { AskUserQuestionParams } from '@vibe-forge/core'
4
+ import { connectionManager } from '#~/connectionManager.js'
5
+
6
+ export function useChatInteraction({ sessionId }: { sessionId?: string }) {
7
+ const [interactionRequest, setInteractionRequest] = useState<{ id: string; payload: AskUserQuestionParams } | null>(
8
+ null
9
+ )
10
+
11
+ const handleInteractionResponse = useCallback((id: string, data: string | string[]) => {
12
+ if (!sessionId) return
13
+ connectionManager.send(sessionId, {
14
+ type: 'interaction_response',
15
+ id,
16
+ data
17
+ })
18
+ setInteractionRequest(null)
19
+ }, [sessionId])
20
+
21
+ return {
22
+ interactionRequest,
23
+ setInteractionRequest,
24
+ handleInteractionResponse
25
+ }
26
+ }
@@ -1,17 +1,17 @@
1
- import React, { useEffect, useMemo, useState } from 'react'
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react'
2
2
  import { useTranslation } from 'react-i18next'
3
3
  import useSWR from 'swr'
4
4
 
5
5
  import type { ConfigResponse, ModelServiceConfig, RecommendedModelConfig } from '@vibe-forge/core'
6
- import { getConfig } from '../../api'
6
+ import { getConfig } from '#~/api.js'
7
7
 
8
- interface ModelSelectOption {
8
+ export interface ModelSelectOption {
9
9
  value: string
10
10
  label: React.ReactNode
11
11
  searchText: string
12
12
  }
13
13
 
14
- interface ModelSelectGroup {
14
+ export interface ModelSelectGroup {
15
15
  label: React.ReactNode
16
16
  options: ModelSelectOption[]
17
17
  }
@@ -46,7 +46,9 @@ export function useChatModels() {
46
46
  const availableModels = useMemo(() => {
47
47
  const list: Array<{ model: string; serviceKey: string; serviceTitle: string }> = []
48
48
  for (const [serviceKey, serviceValue] of modelServiceEntries) {
49
- const service = (serviceValue != null && typeof serviceValue === 'object') ? serviceValue as ModelServiceConfig : undefined
49
+ const service = (serviceValue != null && typeof serviceValue === 'object')
50
+ ? serviceValue as ModelServiceConfig
51
+ : undefined
50
52
  const serviceTitle = service?.title?.trim() !== '' ? service?.title ?? '' : serviceKey
51
53
  const models = Array.isArray(service?.models) ? service?.models.filter(item => typeof item === 'string') : []
52
54
  for (const model of models) {
@@ -60,8 +62,24 @@ export function useChatModels() {
60
62
  const availableModelKey = useMemo(() => availableModelValues.join('|'), [availableModelValues])
61
63
  const availableModelSet = useMemo(() => new Set(availableModelValues), [availableModelKey])
62
64
  const hasAvailableModels = availableModelValues.length > 0
65
+ const modelToService = useMemo(() => {
66
+ const map = new Map<string, { key: string; title: string }>()
67
+ for (const entry of availableModels) {
68
+ if (!map.has(entry.model)) {
69
+ map.set(entry.model, { key: entry.serviceKey, title: entry.serviceTitle })
70
+ }
71
+ }
72
+ return map
73
+ }, [availableModels])
63
74
  const defaultModelService = configRes?.sources?.merged?.general?.defaultModelService
64
75
  const defaultModel = configRes?.sources?.merged?.general?.defaultModel
76
+ 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])
65
83
  const resolvedDefaultModel = useMemo(() => {
66
84
  if (!hasAvailableModels) return undefined
67
85
  if (defaultModel && availableModelSet.has(defaultModel)) return defaultModel
@@ -71,7 +89,17 @@ export function useChatModels() {
71
89
  if (models.length > 0) return models[0]
72
90
  }
73
91
  return availableModelValues[0]
74
- }, [availableModelSet, availableModelValues, defaultModel, defaultModelService, hasAvailableModels, mergedModelServices])
92
+ }, [
93
+ availableModelSet,
94
+ availableModelValues,
95
+ defaultModel,
96
+ defaultModelService,
97
+ hasAvailableModels,
98
+ mergedModelServices
99
+ ])
100
+ const selectedModelWithService = useMemo(() => (
101
+ formatModelWithService(selectedModel)
102
+ ), [formatModelWithService, selectedModel])
75
103
 
76
104
  useEffect(() => {
77
105
  if (!hasAvailableModels) {
@@ -126,13 +154,6 @@ export function useChatModels() {
126
154
  }
127
155
  }
128
156
 
129
- const modelToService = new Map<string, { key: string; title: string }>()
130
- for (const entry of availableModels) {
131
- if (!modelToService.has(entry.model)) {
132
- modelToService.set(entry.model, { key: entry.serviceKey, title: entry.serviceTitle })
133
- }
134
- }
135
-
136
157
  const resolveFirstAlias = (modelsAlias: Record<string, string[]> | undefined, model: string) => {
137
158
  if (!modelsAlias) return undefined
138
159
  for (const [alias, aliasModels] of Object.entries(modelsAlias)) {
@@ -144,7 +165,9 @@ export function useChatModels() {
144
165
 
145
166
  const serviceGroups = modelServiceEntries
146
167
  .map(([serviceKey, serviceValue]) => {
147
- const service = (serviceValue != null && typeof serviceValue === 'object') ? serviceValue as ModelServiceConfig : undefined
168
+ const service = (serviceValue != null && typeof serviceValue === 'object')
169
+ ? serviceValue as ModelServiceConfig
170
+ : undefined
148
171
  const serviceTitle = service?.title?.trim() !== '' ? service?.title ?? '' : serviceKey
149
172
  const groupTitle = serviceTitle?.trim() !== '' ? serviceTitle : serviceKey
150
173
  const serviceDescription = service?.description
@@ -213,10 +236,18 @@ export function useChatModels() {
213
236
  })
214
237
  }
215
238
  return [...groups, ...serviceGroups]
216
- }, [availableModelSet, availableModels, mergedModelServices, modelServiceEntries, recommendedModels, t])
239
+ }, [
240
+ availableModelSet,
241
+ modelToService,
242
+ mergedModelServices,
243
+ modelServiceEntries,
244
+ recommendedModels,
245
+ t
246
+ ])
217
247
 
218
248
  return {
219
249
  selectedModel,
250
+ selectedModelWithService,
220
251
  setSelectedModel,
221
252
  modelOptions,
222
253
  hasAvailableModels
@@ -0,0 +1,47 @@
1
+ import { useEffect, useMemo, useState } from 'react'
2
+ import type { ReactNode } from 'react'
3
+
4
+ export type PermissionMode = 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions'
5
+
6
+ const PERMISSION_MODE_STORAGE_KEY = 'vf_chat_permission_mode'
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'
14
+ }
15
+
16
+ export function useChatPermissionMode() {
17
+ const [permissionMode, setPermissionMode] = useState<PermissionMode>('default')
18
+
19
+ useEffect(() => {
20
+ try {
21
+ const raw = localStorage.getItem(PERMISSION_MODE_STORAGE_KEY)
22
+ if (raw && isPermissionMode(raw)) {
23
+ setPermissionMode(raw)
24
+ }
25
+ } catch {}
26
+ }, [])
27
+
28
+ useEffect(() => {
29
+ try {
30
+ localStorage.setItem(PERMISSION_MODE_STORAGE_KEY, permissionMode)
31
+ } catch {}
32
+ }, [permissionMode])
33
+
34
+ const permissionModeOptions = useMemo<Array<{ value: PermissionMode; label: ReactNode }>>(() => ([
35
+ { value: 'default', label: '默认' },
36
+ { value: 'acceptEdits', label: '接受编辑' },
37
+ { value: 'plan', label: '计划' },
38
+ { value: 'dontAsk', label: '不询问' },
39
+ { value: 'bypassPermissions', label: '跳过权限' }
40
+ ]), [])
41
+
42
+ return {
43
+ permissionMode,
44
+ setPermissionMode,
45
+ permissionModeOptions
46
+ }
47
+ }
@@ -0,0 +1,51 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+
3
+ const SCROLL_THRESHOLD = 80
4
+
5
+ export function useChatScroll({ messagesLength }: { messagesLength: number }) {
6
+ const messagesEndRef = useRef<HTMLDivElement>(null)
7
+ const messagesContainerRef = useRef<HTMLDivElement>(null)
8
+ const messagesContentRef = useRef<HTMLDivElement>(null)
9
+ const [showScrollBottom, setShowScrollBottom] = useState(false)
10
+
11
+ const updateScrollState = useCallback(() => {
12
+ const container = messagesContainerRef.current
13
+ if (!container) return
14
+ const distanceToBottom = container.scrollHeight - (container.scrollTop + container.clientHeight)
15
+ setShowScrollBottom(distanceToBottom > SCROLL_THRESHOLD)
16
+ }, [])
17
+
18
+ const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {
19
+ setTimeout(() => {
20
+ if (messagesContainerRef.current) {
21
+ messagesContainerRef.current.scrollTo({
22
+ top: messagesContainerRef.current.scrollHeight,
23
+ behavior
24
+ })
25
+ }
26
+ }, 50)
27
+ }, [])
28
+
29
+ useEffect(() => {
30
+ const container = messagesContainerRef.current
31
+ if (!container) return
32
+ updateScrollState()
33
+ const handleScroll = () => updateScrollState()
34
+ container.addEventListener('scroll', handleScroll, { passive: true })
35
+ return () => {
36
+ container.removeEventListener('scroll', handleScroll)
37
+ }
38
+ }, [updateScrollState])
39
+
40
+ useEffect(() => {
41
+ updateScrollState()
42
+ }, [updateScrollState, messagesLength])
43
+
44
+ return {
45
+ messagesEndRef,
46
+ messagesContainerRef,
47
+ messagesContentRef,
48
+ showScrollBottom,
49
+ scrollToBottom
50
+ }
51
+ }