@vibe-forge/client 0.3.0 → 0.5.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 (163) hide show
  1. package/cli.cjs +2 -1
  2. package/dist/assets/{arc-CwMXUVsq.js → arc-C4ymrcSQ.js} +1 -1
  3. package/dist/assets/{blockDiagram-c4efeb88-CGxJV7KJ.js → blockDiagram-c4efeb88-CeB7-kgP.js} +1 -1
  4. package/dist/assets/{c4Diagram-c83219d4-BKhin7cY.js → c4Diagram-c83219d4-C935Im8S.js} +1 -1
  5. package/dist/assets/channel-84s1ACzD.js +1 -0
  6. package/dist/assets/{classDiagram-beda092f-BASmn22R.js → classDiagram-beda092f-B9IV13KI.js} +1 -1
  7. package/dist/assets/{classDiagram-v2-2358418a-BUk9rNBX.js → classDiagram-v2-2358418a-CXF_K4fE.js} +1 -1
  8. package/dist/assets/clone-B2E8tddE.js +1 -0
  9. package/dist/assets/{createText-1719965b-2XqnWjQY.js → createText-1719965b-DwX8iC5F.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-9P1uH1RE.js} +1 -1
  15. package/dist/assets/{erDiagram-0228fc6a-CCR2or72.js → erDiagram-0228fc6a-ixeGTFvg.js} +1 -1
  16. package/dist/assets/{flowDb-c6c81e3f-B72HWT9x.js → flowDb-c6c81e3f-G1gSTTBI.js} +1 -1
  17. package/dist/assets/{flowDiagram-50d868cf-WOi0KARY.js → flowDiagram-50d868cf-CzrG99nD.js} +1 -1
  18. package/dist/assets/flowDiagram-v2-4f6560a1-CJfJYbME.js +1 -0
  19. package/dist/assets/{flowchart-elk-definition-6af322e1-i_Yd0LCE.js → flowchart-elk-definition-6af322e1-sFCoysWa.js} +1 -1
  20. package/dist/assets/{ganttDiagram-a2739b55-CFH9zF14.js → ganttDiagram-a2739b55-Ccsk_Lru.js} +1 -1
  21. package/dist/assets/{gitGraphDiagram-82fe8481-DglKfMze.js → gitGraphDiagram-82fe8481-CwathJ6H.js} +1 -1
  22. package/dist/assets/{graph-BKbBNGPf.js → graph-DRCU-8Rz.js} +1 -1
  23. package/dist/assets/{index-5325376f-BK7F9nSl.js → index-5325376f-Bq-fg2i_.js} +1 -1
  24. package/dist/assets/index-CHMuZ5-1.css +1 -0
  25. package/dist/assets/index-cGZvDhhU.js +542 -0
  26. package/dist/assets/{infoDiagram-8eee0895-BLFL77_D.js → infoDiagram-8eee0895-JBcUkJ6T.js} +1 -1
  27. package/dist/assets/{journeyDiagram-c64418c1-CS9XctDL.js → journeyDiagram-c64418c1-DsdQU-R8.js} +1 -1
  28. package/dist/assets/{layout-By3JZZGt.js → layout-s0slG1OL.js} +1 -1
  29. package/dist/assets/{line-9GUsXbwv.js → line-CymFqgW6.js} +1 -1
  30. package/dist/assets/{linear-DzGV4E9N.js → linear-lDQVZ6aQ.js} +1 -1
  31. package/dist/assets/{mermaid.core-CG3Ib42Q.js → mermaid.core-Cmlqga_E.js} +6 -6
  32. package/dist/assets/{mindmap-definition-8da855dc-WQ3LPKJU.js → mindmap-definition-8da855dc-CqqTDJn_.js} +1 -1
  33. package/dist/assets/{pieDiagram-a8764435-DHVIUZiN.js → pieDiagram-a8764435-BL2Ajx7Z.js} +1 -1
  34. package/dist/assets/{quadrantDiagram-1e28029f-C3G9Ye8-.js → quadrantDiagram-1e28029f-ClL_3ASt.js} +1 -1
  35. package/dist/assets/{requirementDiagram-08caed73-C9ES1D5G.js → requirementDiagram-08caed73-CB1RgE3K.js} +1 -1
  36. package/dist/assets/{sankeyDiagram-a04cb91d-B4BKXclQ.js → sankeyDiagram-a04cb91d-tgleEYiD.js} +1 -1
  37. package/dist/assets/{sequenceDiagram-c5b8d532-DrgEb25G.js → sequenceDiagram-c5b8d532-DlatQT5R.js} +1 -1
  38. package/dist/assets/{stateDiagram-1ecb1508-CF1XWARJ.js → stateDiagram-1ecb1508-B--MLqRs.js} +1 -1
  39. package/dist/assets/{stateDiagram-v2-c2b004d7-IO3i3yXv.js → stateDiagram-v2-c2b004d7-CRMZ6Dpx.js} +1 -1
  40. package/dist/assets/{styles-b4e223ce-DACN9aSc.js → styles-b4e223ce-CPiYHfUz.js} +1 -1
  41. package/dist/assets/{styles-ca3715f6-bekm2WLP.js → styles-ca3715f6-B9UKPAzX.js} +1 -1
  42. package/dist/assets/{styles-d45a18b0-OzTDVBb8.js → styles-d45a18b0-BC1Ak1So.js} +1 -1
  43. package/dist/assets/{svgDrawCommon-b86b1483-BWroJerr.js → svgDrawCommon-b86b1483-DV8R0g-n.js} +1 -1
  44. package/dist/assets/{timeline-definition-faaaa080-CCfRNigO.js → timeline-definition-faaaa080-CiqGS5DC.js} +1 -1
  45. package/dist/assets/{xychartDiagram-f5964ef8-C3cbfVqN.js → xychartDiagram-f5964ef8-h6VSD3GE.js} +1 -1
  46. package/dist/index.html +2 -7
  47. package/index.html +0 -5
  48. package/package.json +12 -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 +84 -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 +43 -29
  74. package/src/components/{chat/CodeBlock.tsx → CodeBlock.tsx} +3 -1
  75. package/src/components/ConfigView.tsx +32 -25
  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 +99 -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} +146 -3
  86. package/src/components/chat/{Sender.tsx → Sender/Sender.tsx} +183 -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/ConfigSourceSwitch.tsx +12 -34
  122. package/src/components/config/channelDefinitions.ts +6 -0
  123. package/src/components/config/configSchema.ts +10 -1
  124. package/src/components/config/recordEditors/ChannelRecordEditor.scss +1 -0
  125. package/src/components/config/recordEditors/ChannelRecordEditor.tsx +397 -0
  126. package/src/components/config/recordEditors/index.tsx +1 -0
  127. package/src/components/knowledge-base/components/RuleItem.tsx +1 -1
  128. package/src/components/knowledge-base/components/SpecItem.tsx +1 -1
  129. package/src/components/sidebar/SessionItem.scss +17 -0
  130. package/src/components/sidebar/SessionItem.tsx +21 -13
  131. package/src/hooks/chat/use-chat-adapter.ts +81 -0
  132. package/src/hooks/chat/use-chat-interaction.ts +26 -0
  133. package/src/{components/chat/useChatModels.tsx → hooks/chat/use-chat-models.tsx} +117 -22
  134. package/src/hooks/chat/use-chat-permission-mode.ts +47 -0
  135. package/src/hooks/chat/use-chat-scroll.ts +51 -0
  136. package/src/hooks/chat/use-chat-session-actions.ts +153 -0
  137. package/src/hooks/chat/use-chat-session-messages.ts +262 -0
  138. package/src/hooks/chat/use-chat-session.ts +63 -0
  139. package/src/hooks/chat/use-chat-view.ts +39 -0
  140. package/src/main.tsx +10 -13
  141. package/src/resources/adapters.ts +20 -0
  142. package/src/resources/locales/en.json +66 -0
  143. package/src/resources/locales/zh.json +66 -0
  144. package/src/runtime-config.ts +52 -0
  145. package/src/vite-env.d.ts +11 -0
  146. package/src/ws.ts +5 -3
  147. package/vite.config.ts +12 -4
  148. package/dist/assets/channel-jbCEHqbG.js +0 -1
  149. package/dist/assets/clone-CCRKqS4L.js +0 -1
  150. package/dist/assets/flowDiagram-v2-4f6560a1-Baslbgn4.js +0 -1
  151. package/dist/assets/index-B0qfCb1G.css +0 -1
  152. package/dist/assets/index-CNo75dYr.js +0 -497
  153. package/src/components/chat/ToolCallBox.scss +0 -137
  154. package/src/components/chat/useChatSession.ts +0 -370
  155. /package/src/components/{chat/CodeBlock.scss → CodeBlock.scss} +0 -0
  156. /package/src/components/chat/{MessageFooter.tsx → Messages/MessageFooter.tsx} +0 -0
  157. /package/src/components/chat/{CompletionMenu.scss → Sender/CompletionMenu.scss} +0 -0
  158. /package/src/components/chat/{CompletionMenu.tsx → Sender/CompletionMenu.tsx} +0 -0
  159. /package/src/components/chat/{ThinkingStatus.scss → Sender/ThinkingStatus.scss} +0 -0
  160. /package/src/components/chat/{ThinkingStatus.tsx → Sender/ThinkingStatus.tsx} +0 -0
  161. /package/src/components/chat/{ToolCallBox.tsx → tools/core/ToolCallBox.tsx} +0 -0
  162. /package/src/components/chat/{ToolGroup.scss → tools/core/ToolGroup.scss} +0 -0
  163. /package/src/{components/chat/safeSerialize.ts → utils/safe-serialize.ts} +0 -0
@@ -6,11 +6,12 @@ import React, { useEffect, useRef, useState } from 'react'
6
6
  import { useTranslation } from 'react-i18next'
7
7
  import useSWR from 'swr'
8
8
 
9
- import type { AskUserQuestionParams, SessionInfo, SessionStatus } from '@vibe-forge/core'
9
+ import type { AskUserQuestionParams, ChatMessageContent, SessionInfo, SessionStatus } from '@vibe-forge/core'
10
10
  import type { CompletionItem } from './CompletionMenu'
11
+ import type { PermissionMode } from '#~/hooks/chat/use-chat-permission-mode'
11
12
  import { CompletionMenu } from './CompletionMenu'
12
13
  import { ThinkingStatus } from './ThinkingStatus'
13
- import { isShortcutMatch } from '../../utils/shortcutUtils'
14
+ import { isShortcutMatch } from '../../../utils/shortcutUtils'
14
15
 
15
16
  const { TextArea } = Input
16
17
 
@@ -25,8 +26,17 @@ interface ModelSelectGroup {
25
26
  options: ModelSelectOption[]
26
27
  }
27
28
 
29
+ interface PendingImage {
30
+ id: string
31
+ url: string
32
+ name?: string
33
+ size?: number
34
+ mimeType?: string
35
+ }
36
+
28
37
  export function Sender({
29
38
  onSend,
39
+ onSendContent,
30
40
  sessionStatus,
31
41
  onInterrupt,
32
42
  onClear,
@@ -37,9 +47,16 @@ export function Sender({
37
47
  modelOptions,
38
48
  selectedModel,
39
49
  onModelChange,
50
+ permissionMode,
51
+ permissionModeOptions,
52
+ onPermissionModeChange,
53
+ selectedAdapter,
54
+ adapterOptions,
55
+ onAdapterChange,
40
56
  modelUnavailable
41
57
  }: {
42
58
  onSend: (text: string) => void
59
+ onSendContent: (content: ChatMessageContent[]) => void
43
60
  sessionStatus?: SessionStatus
44
61
  onInterrupt: () => void
45
62
  onClear?: () => void
@@ -50,6 +67,12 @@ export function Sender({
50
67
  modelOptions?: ModelSelectGroup[]
51
68
  selectedModel?: string
52
69
  onModelChange?: (model: string) => void
70
+ permissionMode: PermissionMode
71
+ permissionModeOptions: Array<{ value: PermissionMode; label: React.ReactNode }>
72
+ onPermissionModeChange: (mode: PermissionMode) => void
73
+ selectedAdapter?: string
74
+ adapterOptions?: Array<{ value: string; label: React.ReactNode }>
75
+ onAdapterChange?: (adapter: string) => void
53
76
  modelUnavailable?: boolean
54
77
  }) {
55
78
  const { t } = useTranslation()
@@ -63,7 +86,9 @@ export function Sender({
63
86
  const [showToolsList, setShowToolsList] = useState(false)
64
87
  const textareaRef = useRef<TextAreaRef>(null)
65
88
  const toolsRef = useRef<HTMLDivElement>(null)
89
+ const fileInputRef = useRef<HTMLInputElement>(null)
66
90
  const isMac = navigator.platform.includes('Mac')
91
+ const [pendingImages, setPendingImages] = useState<PendingImage[]>([])
67
92
 
68
93
  const { data: configRes } = useSWR<{
69
94
  sources?: {
@@ -96,8 +121,54 @@ export function Sender({
96
121
  const [historyIndex, setHistoryIndex] = useState(-1)
97
122
  const [draft, setDraft] = useState('')
98
123
 
124
+ const readFileAsDataUrl = (file: File) => {
125
+ return new Promise<string>((resolve, reject) => {
126
+ const reader = new FileReader()
127
+ reader.onload = () => {
128
+ resolve(typeof reader.result === 'string' ? reader.result : '')
129
+ }
130
+ reader.onerror = () => {
131
+ reject(new Error('read_failed'))
132
+ }
133
+ reader.readAsDataURL(file)
134
+ })
135
+ }
136
+
137
+ const addImageFiles = async (files: File[]) => {
138
+ const maxSize = 5 * 1024 * 1024
139
+ for (const file of files) {
140
+ if (!file.type.startsWith('image/')) continue
141
+ if (file.size > maxSize) {
142
+ void message.error(t('chat.imageTooLarge'))
143
+ continue
144
+ }
145
+ try {
146
+ const url = await readFileAsDataUrl(file)
147
+ if (url === '') {
148
+ void message.error(t('chat.imageReadFailed'))
149
+ continue
150
+ }
151
+ setPendingImages(prev => [
152
+ ...prev,
153
+ {
154
+ id: globalThis.crypto?.randomUUID
155
+ ? globalThis.crypto.randomUUID()
156
+ : `img-${Date.now()}-${Math.random().toString(16).slice(2)}`,
157
+ url,
158
+ name: file.name,
159
+ size: file.size,
160
+ mimeType: file.type
161
+ }
162
+ ])
163
+ } catch (err) {
164
+ void message.error(t('chat.imageReadFailed'))
165
+ }
166
+ }
167
+ }
168
+
99
169
  const handleSend = () => {
100
- if (input.trim() === '' || isThinking) return
170
+ if (isThinking) return
171
+ if (input.trim() === '' && pendingImages.length === 0) return
101
172
 
102
173
  if (modelUnavailable) {
103
174
  void message.warning(t('chat.modelConfigRequired'))
@@ -105,12 +176,31 @@ export function Sender({
105
176
  }
106
177
 
107
178
  if (interactionRequest != null && onInteractionResponse != null) {
179
+ if (pendingImages.length > 0) {
180
+ void message.warning(t('chat.imageNotSupportedInInteraction'))
181
+ return
182
+ }
108
183
  onInteractionResponse(interactionRequest.id, input.trim())
109
184
  setInput('')
110
185
  return
111
186
  }
112
187
 
113
- onSend(input)
188
+ if (pendingImages.length > 0) {
189
+ const content: ChatMessageContent[] = []
190
+ if (input.trim() !== '') {
191
+ content.push({ type: 'text', text: input.trim() })
192
+ }
193
+ content.push(...pendingImages.map(img => ({
194
+ type: 'image',
195
+ url: img.url,
196
+ name: img.name,
197
+ size: img.size,
198
+ mimeType: img.mimeType
199
+ })))
200
+ onSendContent(content)
201
+ } else {
202
+ onSend(input)
203
+ }
114
204
 
115
205
  // Save to local storage history
116
206
  try {
@@ -122,11 +212,48 @@ export function Sender({
122
212
  }
123
213
 
124
214
  setInput('')
215
+ setPendingImages([])
125
216
  setDraft('')
126
217
  setShowCompletion(false)
127
218
  setHistoryIndex(-1)
128
219
  }
129
220
 
221
+ const handleImageUpload = () => {
222
+ if (isThinking) return
223
+ if (modelUnavailable) {
224
+ void message.warning(t('chat.modelConfigRequired'))
225
+ return
226
+ }
227
+ if (interactionRequest != null) {
228
+ void message.warning(t('chat.imageNotSupportedInInteraction'))
229
+ return
230
+ }
231
+ fileInputRef.current?.click()
232
+ }
233
+
234
+ const handleImageFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
235
+ const fileList = Array.from(event.target.files ?? [])
236
+ if (fileList.length === 0) return
237
+ await addImageFiles(fileList)
238
+ event.target.value = ''
239
+ }
240
+
241
+ const handleRemovePendingImage = (id: string) => {
242
+ setPendingImages(prev => prev.filter(img => img.id !== id))
243
+ }
244
+
245
+ const handlePaste = async (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
246
+ const items = Array.from(event.clipboardData?.items ?? [])
247
+ const files = items
248
+ .filter(item => item.kind === 'file' && item.type.startsWith('image/'))
249
+ .map(item => item.getAsFile())
250
+ .filter((file): file is File => file != null)
251
+ if (files.length > 0) {
252
+ event.preventDefault()
253
+ await addImageFiles(files)
254
+ }
255
+ }
256
+
130
257
  const clearInputValue = () => {
131
258
  if (input === '') return
132
259
  setInput('')
@@ -422,6 +549,18 @@ export function Sender({
422
549
  {t('chat.modelConfigRequired')}
423
550
  </div>
424
551
  )}
552
+ {pendingImages.length > 0 && (
553
+ <div className='pending-images'>
554
+ {pendingImages.map(img => (
555
+ <div key={img.id} className='pending-image'>
556
+ <img src={img.url} alt={img.name ?? 'image'} />
557
+ <div className='pending-image-remove' onClick={() => handleRemovePendingImage(img.id)}>
558
+ <span className='material-symbols-rounded'>close</span>
559
+ </div>
560
+ </div>
561
+ ))}
562
+ </div>
563
+ )}
425
564
  {showCompletion && (
426
565
  <CompletionMenu
427
566
  items={completionItems}
@@ -437,12 +576,21 @@ export function Sender({
437
576
  value={input}
438
577
  onChange={handleInputChange}
439
578
  onKeyDown={handleKeyDown}
579
+ onPaste={handlePaste}
440
580
  autoSize={{ minRows: 1, maxRows: 10 }}
441
581
  variant='borderless'
442
582
  disabled={modelUnavailable}
443
583
  />
444
584
 
445
585
  <div className='chat-input-toolbar'>
586
+ <input
587
+ ref={fileInputRef}
588
+ type='file'
589
+ accept='image/*'
590
+ multiple
591
+ onChange={handleImageFileChange}
592
+ className='file-input-hidden'
593
+ />
446
594
  <div className='toolbar-left'>
447
595
  <Tooltip title='快捷指令'>
448
596
  <span>
@@ -467,7 +615,7 @@ export function Sender({
467
615
  </Tooltip>
468
616
  <Tooltip title='上传图片'>
469
617
  <span>
470
- <div className='toolbar-btn' onClick={() => void message.info('图片上传功能尚不支持')}>
618
+ <div className='toolbar-btn' onClick={handleImageUpload}>
471
619
  <span className='material-symbols-rounded'>image</span>
472
620
  </div>
473
621
  </span>
@@ -504,6 +652,22 @@ export function Sender({
504
652
  </div>
505
653
 
506
654
  <div className='toolbar-right'>
655
+ {adapterOptions && adapterOptions.length > 1 && (
656
+ <Select
657
+ className='adapter-select'
658
+ classNames={{ popup: { root: 'adapter-select-popup' } }}
659
+ value={selectedAdapter}
660
+ options={adapterOptions}
661
+ showSearch={false}
662
+ allowClear={false}
663
+ disabled={modelUnavailable || isThinking}
664
+ onChange={(value) => onAdapterChange?.(value)}
665
+ placeholder={t('chat.adapterSelectPlaceholder', { defaultValue: 'Adapter' })}
666
+ optionLabelProp='label'
667
+ popupMatchSelectWidth={false}
668
+ />
669
+ )}
670
+
507
671
  <Select
508
672
  className='model-select'
509
673
  classNames={{ popup: { root: 'model-select-popup' } }}
@@ -522,6 +686,20 @@ export function Sender({
522
686
  popupMatchSelectWidth={false}
523
687
  />
524
688
 
689
+ <Select
690
+ className='permission-mode-select'
691
+ classNames={{ popup: { root: 'permission-mode-select-popup' } }}
692
+ value={permissionMode}
693
+ options={permissionModeOptions}
694
+ showSearch={false}
695
+ allowClear={false}
696
+ disabled={modelUnavailable || isThinking}
697
+ onChange={(value) => onPermissionModeChange(value)}
698
+ placeholder='权限模式'
699
+ optionLabelProp='label'
700
+ popupMatchSelectWidth={false}
701
+ />
702
+
525
703
  <div
526
704
  className={`chat-send-btn ${input.trim() !== '' && !modelUnavailable ? 'active' : ''} ${isThinking ? 'thinking' : ''} ${modelUnavailable ? 'disabled' : ''}`}
527
705
  onClick={modelUnavailable ? undefined : (isThinking ? onInterrupt : handleSend)}
@@ -1,10 +1,172 @@
1
1
  import type { ChatMessageContent } from '@vibe-forge/core'
2
- import React from 'react'
3
2
  import { useTranslation } from 'react-i18next'
4
- import { CodeBlock } from '../CodeBlock'
5
- import { MarkdownContent } from '../MarkdownContent'
6
- import { ToolCallBox } from '../ToolCallBox'
7
- import { safeJsonStringify } from '../safeSerialize'
3
+ import { CodeBlock } from '#~/components/CodeBlock'
4
+ import { MarkdownContent } from '#~/components/MarkdownContent'
5
+ import { ToolCallBox } from './core/ToolCallBox'
6
+ import { safeJsonStringify, toSerializable } from '#~/utils/safe-serialize'
7
+
8
+ interface StructuredTextBlock {
9
+ type: 'text'
10
+ text: string
11
+ format: 'text' | 'markdown'
12
+ }
13
+
14
+ interface StructuredImageBlock {
15
+ type: 'image'
16
+ src: string
17
+ alt?: string
18
+ title?: string
19
+ width?: number
20
+ height?: number
21
+ }
22
+
23
+ type StructuredBlock = StructuredTextBlock | StructuredImageBlock
24
+
25
+ function parseStructuredInput(value: unknown) {
26
+ if (typeof value !== 'string') {
27
+ return value
28
+ }
29
+ const trimmed = value.trim()
30
+ if (
31
+ (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
32
+ (trimmed.startsWith('[') && trimmed.endsWith(']'))
33
+ ) {
34
+ try {
35
+ return JSON.parse(trimmed)
36
+ } catch {
37
+ return value
38
+ }
39
+ }
40
+ return value
41
+ }
42
+
43
+ function resolveImageSource(value: Record<string, unknown>) {
44
+ const directUrl = typeof value.url === 'string'
45
+ ? value.url
46
+ : typeof value.src === 'string'
47
+ ? value.src
48
+ : typeof value.image_url === 'string'
49
+ ? value.image_url
50
+ : typeof value.imageUrl === 'string'
51
+ ? value.imageUrl
52
+ : typeof value.dataUrl === 'string'
53
+ ? value.dataUrl
54
+ : null
55
+ if (directUrl) {
56
+ return directUrl
57
+ }
58
+ const source = value.source != null && typeof value.source === 'object'
59
+ ? (value.source as Record<string, unknown>)
60
+ : null
61
+ const data = typeof value.data === 'string'
62
+ ? value.data
63
+ : typeof value.base64 === 'string'
64
+ ? value.base64
65
+ : source != null && typeof source.data === 'string'
66
+ ? source.data
67
+ : null
68
+ if (!data) {
69
+ return null
70
+ }
71
+ const mimeType = typeof value.mimeType === 'string'
72
+ ? value.mimeType
73
+ : typeof value.mime_type === 'string'
74
+ ? value.mime_type
75
+ : source != null && typeof source.media_type === 'string'
76
+ ? source.media_type
77
+ : source != null && typeof source.mimeType === 'string'
78
+ ? source.mimeType
79
+ : source != null && typeof source.mime_type === 'string'
80
+ ? source.mime_type
81
+ : 'image/png'
82
+ return `data:${mimeType};base64,${data}`
83
+ }
84
+
85
+ function parseBlock(value: unknown): StructuredBlock | null {
86
+ if (value == null || typeof value !== 'object') {
87
+ return null
88
+ }
89
+ const obj = value as Record<string, unknown>
90
+ const rawType = typeof obj.type === 'string' ? obj.type.toLowerCase() : ''
91
+ if (rawType === 'text' || rawType === 'markdown' || rawType === 'md') {
92
+ const text = typeof obj.text === 'string'
93
+ ? obj.text
94
+ : typeof obj.content === 'string'
95
+ ? obj.content
96
+ : null
97
+ if (text == null) {
98
+ return null
99
+ }
100
+ const rawFormat = typeof obj.format === 'string' ? obj.format.toLowerCase() : 'markdown'
101
+ const format = rawType === 'text'
102
+ ? (rawFormat === 'text' || rawFormat === 'plain' ? 'text' : 'markdown')
103
+ : 'markdown'
104
+ return { type: 'text', text, format }
105
+ }
106
+ if (rawType === 'image') {
107
+ const src = resolveImageSource(obj)
108
+ if (!src) {
109
+ return null
110
+ }
111
+ const alt = typeof obj.alt === 'string' ? obj.alt : undefined
112
+ const title = typeof obj.title === 'string' ? obj.title : undefined
113
+ const width = typeof obj.width === 'number' ? obj.width : undefined
114
+ const height = typeof obj.height === 'number' ? obj.height : undefined
115
+ return { type: 'image', src, alt, title, width, height }
116
+ }
117
+ return null
118
+ }
119
+
120
+ function getStructuredBlocks(value: unknown): StructuredBlock[] | null {
121
+ const serializable = toSerializable(value)
122
+ const parsed = parseStructuredInput(serializable)
123
+ if (Array.isArray(parsed)) {
124
+ const blocks = parsed.map(parseBlock)
125
+ return blocks.every(Boolean) ? (blocks as StructuredBlock[]) : null
126
+ }
127
+ if (parsed != null && typeof parsed === 'object') {
128
+ const container = parsed as Record<string, unknown>
129
+ const content = container.content ?? container.items ?? container.blocks
130
+ if (Array.isArray(content)) {
131
+ const blocks = content.map(parseBlock)
132
+ return blocks.every(Boolean) ? (blocks as StructuredBlock[]) : null
133
+ }
134
+ }
135
+ const single = parseBlock(parsed)
136
+ return single ? [single] : null
137
+ }
138
+
139
+ function StructuredToolResult({ blocks }: { blocks: StructuredBlock[] }) {
140
+ return (
141
+ <div className='tool-result-structured'>
142
+ {blocks.map((block, index) => {
143
+ if (block.type === 'text') {
144
+ return (
145
+ <div className='tool-result-text' key={`text-${index}`}>
146
+ {block.format === 'markdown'
147
+ ? <MarkdownContent content={block.text} />
148
+ : <div className='tool-result-text-content'>{block.text}</div>}
149
+ </div>
150
+ )
151
+ }
152
+ return (
153
+ <div className='tool-result-image-wrapper' key={`image-${index}`}>
154
+ <img
155
+ className='tool-result-image'
156
+ src={block.src}
157
+ alt={block.alt ?? ''}
158
+ width={block.width}
159
+ height={block.height}
160
+ />
161
+ {block.title != null && block.title.length > 0 && (
162
+ <div className='tool-result-image-caption'>{block.title}</div>
163
+ )}
164
+ </div>
165
+ )
166
+ })}
167
+ </div>
168
+ )
169
+ }
8
170
 
9
171
  export function DefaultTool({
10
172
  item,
@@ -14,17 +176,16 @@ export function DefaultTool({
14
176
  resultItem?: Extract<ChatMessageContent, { type: 'tool_result' }>
15
177
  }) {
16
178
  const { t } = useTranslation()
179
+ const structuredBlocks = resultItem != null ? getStructuredBlocks(resultItem.content) : null
17
180
  return (
18
181
  <div className='tool-group'>
19
182
  <ToolCallBox
20
183
  header={
21
- <>
22
- <span className='material-symbols-rounded' style={{ fontSize: 16 }}>build</span>
23
- <span>{item.name}</span>
24
- <span style={{ color: 'var(--sub-text-color)', fontSize: 11, fontWeight: 400 }}>
25
- {t('chat.tools.call')}
26
- </span>
27
- </>
184
+ <div className='tool-header-content'>
185
+ <span className='material-symbols-rounded tool-header-icon'>build</span>
186
+ <span className='tool-header-title'>{item.name}</span>
187
+ <span className='tool-header-hint'>{t('chat.tools.call')}</span>
188
+ </div>
28
189
  }
29
190
  content={
30
191
  <div className='tool-content'>
@@ -40,20 +201,22 @@ export function DefaultTool({
40
201
  type='result'
41
202
  isError={resultItem.is_error}
42
203
  header={
43
- <>
44
- <span className='material-symbols-rounded' style={{ fontSize: 16 }}>
204
+ <div className='tool-header-content'>
205
+ <span className='material-symbols-rounded tool-header-icon'>
45
206
  {resultItem.is_error === true ? 'error' : 'check_circle'}
46
207
  </span>
47
- <span>{t('chat.result')}</span>
48
- </>
208
+ <span className='tool-header-title'>{t('chat.result')}</span>
209
+ </div>
49
210
  }
50
211
  content={
51
212
  <div className='tool-content'>
52
- {typeof resultItem.content === 'string'
53
- ? (resultItem.content.startsWith('```')
54
- ? <MarkdownContent content={resultItem.content} />
55
- : <CodeBlock code={resultItem.content} lang='text' />)
56
- : <CodeBlock code={safeJsonStringify(resultItem.content, 2)} lang='json' />}
213
+ {structuredBlocks != null
214
+ ? <StructuredToolResult blocks={structuredBlocks} />
215
+ : (typeof resultItem.content === 'string'
216
+ ? (resultItem.content.startsWith('```')
217
+ ? <MarkdownContent content={resultItem.content} />
218
+ : <CodeBlock code={resultItem.content} lang='text' />)
219
+ : <CodeBlock code={safeJsonStringify(resultItem.content, 2)} lang='json' />)}
57
220
  </div>
58
221
  }
59
222
  />
@@ -1,71 +1,87 @@
1
1
  .tool-group.bash-tool {
2
- .bash-header, .result-header {
2
+ .tool-call-header {
3
+ height: auto;
4
+ min-height: 32px;
5
+ }
6
+
7
+ .bash-tool__header {
8
+ width: 100%;
9
+ min-width: 0;
3
10
  display: flex;
4
11
  align-items: center;
5
- gap: 6px;
12
+ gap: 8px;
13
+ }
6
14
 
7
- .status-icon {
8
- font-size: 16px;
9
- }
15
+ .bash-tool__icon {
16
+ font-size: 16px;
17
+ color: var(--sub-text-color);
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ width: 16px;
22
+ height: 16px;
23
+ flex-shrink: 0;
24
+ align-self: center;
25
+ }
10
26
 
11
- .bash-title, .result-title {
12
- font-weight: 600;
13
- }
27
+ .bash-tool__header-main {
28
+ flex: 1;
29
+ min-width: 0;
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: 2px;
14
33
  }
15
34
 
16
- .bash-command-preview {
17
- font-family:
18
- 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
19
- font-size: 11px;
20
- color: var(--text-color);
35
+ .bash-tool__command-row {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 6px;
39
+ min-width: 0;
40
+ }
41
+
42
+ .bash-tool__command-text {
43
+ flex: 1;
44
+ min-width: 0;
21
45
  overflow: hidden;
22
46
  text-overflow: ellipsis;
23
47
  white-space: nowrap;
24
- flex: 1;
25
- }
26
-
27
- .tool-reason {
28
- margin: 0 0 4px 0;
29
48
  font-size: 12px;
30
- color: var(--sub-text-color);
31
- line-height: 1.4;
49
+ color: var(--text-color);
50
+ }
32
51
 
33
- &.markdown-body {
34
- font-size: 12px;
35
- color: inherit;
36
- background-color: transparent;
37
- padding: 0;
52
+ .bash-tool__command-text--clickable {
53
+ cursor: pointer;
54
+ color: var(--text-color);
38
55
 
39
- p {
40
- margin: 0 !important;
41
- padding: 0 !important;
42
- }
56
+ &:hover {
57
+ text-shadow: 0 0 .5px currentColor;
43
58
  }
44
59
  }
45
60
 
46
- .bash-content-scroll {
47
- max-height: 400px;
48
- overflow-y: auto;
61
+ .bash-tool__reason-row {
62
+ display: flex;
63
+ min-width: 0;
64
+ }
49
65
 
50
- .bash-code-wrapper {
51
- background-color: transparent;
52
- border: none;
53
- border-radius: 0;
54
- padding: 0;
55
- }
66
+ .bash-tool__reason-text {
67
+ min-width: 0;
68
+ overflow: hidden;
69
+ text-overflow: ellipsis;
70
+ white-space: nowrap;
71
+ }
56
72
 
57
- &::-webkit-scrollbar {
58
- width: 4px;
59
- }
60
- &::-webkit-scrollbar-track {
61
- background: transparent;
62
- }
63
- &::-webkit-scrollbar-thumb {
64
- background: var(--border-color);
65
- border-radius: 4px;
66
- }
67
- &::-webkit-scrollbar-thumb:hover {
68
- background: var(--sub-text-color);
69
- }
73
+ .bash-tool__header-tags {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 6px;
77
+ flex-shrink: 0;
78
+ }
79
+
80
+ .bash-tool__command-detail {
81
+ margin-bottom: 6px;
82
+ }
83
+
84
+ .tool-scroll {
85
+ max-height: 270px;
70
86
  }
71
87
  }