@vibe-forge/client 0.2.0-alpha.9 → 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 (166) hide show
  1. package/cli.cjs +1 -1
  2. package/dist/assets/{arc-CybT1Fs2.js → arc-DgIxeTMg.js} +1 -1
  3. package/dist/assets/{blockDiagram-c4efeb88-BY5Aoa-D.js → blockDiagram-c4efeb88-CEAob3X9.js} +1 -1
  4. package/dist/assets/{c4Diagram-c83219d4-F42hTbzS.js → c4Diagram-c83219d4-DwIxpDKd.js} +1 -1
  5. package/dist/assets/channel-DhtnrNJ6.js +1 -0
  6. package/dist/assets/{classDiagram-beda092f-D-tIPp-3.js → classDiagram-beda092f-Cz1q8u_0.js} +1 -1
  7. package/dist/assets/{classDiagram-v2-2358418a-J57aCe6u.js → classDiagram-v2-2358418a-CImgTuwd.js} +1 -1
  8. package/dist/assets/clone-7bHB6YkC.js +1 -0
  9. package/dist/assets/{createText-1719965b-ByfEqOF-.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-CMEArkOa.js → edges-96097737-BU8qStzd.js} +1 -1
  15. package/dist/assets/{erDiagram-0228fc6a-Cf8mX2aj.js → erDiagram-0228fc6a-DNA1Fz2L.js} +1 -1
  16. package/dist/assets/{flowDb-c6c81e3f-DG6WKyo7.js → flowDb-c6c81e3f-DjiCStMN.js} +1 -1
  17. package/dist/assets/{flowDiagram-50d868cf-CstUxz-w.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--4CRoQ-H.js → flowchart-elk-definition-6af322e1-DrhIMas7.js} +1 -1
  20. package/dist/assets/{ganttDiagram-a2739b55-DYgHcKd-.js → ganttDiagram-a2739b55-CTZnUP5z.js} +1 -1
  21. package/dist/assets/{gitGraphDiagram-82fe8481-DDSVpfsd.js → gitGraphDiagram-82fe8481-COOW7jTi.js} +1 -1
  22. package/dist/assets/{graph-CRWF39gX.js → graph-CIkpD4Kx.js} +1 -1
  23. package/dist/assets/{index-5325376f-W1hft795.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-D4SHcix6.js → infoDiagram-8eee0895-DQpZ1LVD.js} +1 -1
  27. package/dist/assets/{journeyDiagram-c64418c1-MWgCkVoE.js → journeyDiagram-c64418c1-DoKguIuk.js} +1 -1
  28. package/dist/assets/{layout-C88ObkCf.js → layout-Tnmha8Nh.js} +1 -1
  29. package/dist/assets/{line-C7WAYMt5.js → line-BQR2SOyl.js} +1 -1
  30. package/dist/assets/{linear-C4msxfcU.js → linear-DlG0eemV.js} +1 -1
  31. package/dist/assets/{mermaid.core-Cabag9SZ.js → mermaid.core-BnwYO0He.js} +6 -6
  32. package/dist/assets/{mindmap-definition-8da855dc-CeS8ETXx.js → mindmap-definition-8da855dc-BllYwDID.js} +1 -1
  33. package/dist/assets/{pieDiagram-a8764435-BvjyKnq5.js → pieDiagram-a8764435-DwCkhPVc.js} +1 -1
  34. package/dist/assets/{quadrantDiagram-1e28029f-DzYvpbNM.js → quadrantDiagram-1e28029f-c40GKTU0.js} +1 -1
  35. package/dist/assets/{requirementDiagram-08caed73-DHIoDbyo.js → requirementDiagram-08caed73-DnQp2Tk6.js} +1 -1
  36. package/dist/assets/{sankeyDiagram-a04cb91d-BFSGnQGs.js → sankeyDiagram-a04cb91d-CnJrs13b.js} +1 -1
  37. package/dist/assets/{sequenceDiagram-c5b8d532-_LM3BJ5-.js → sequenceDiagram-c5b8d532-1YBwnpKu.js} +1 -1
  38. package/dist/assets/{stateDiagram-1ecb1508-DwORjOzl.js → stateDiagram-1ecb1508-BFBxQ6Fh.js} +1 -1
  39. package/dist/assets/{stateDiagram-v2-c2b004d7-B4cAWWz1.js → stateDiagram-v2-c2b004d7-Dmechvv2.js} +1 -1
  40. package/dist/assets/{styles-b4e223ce-D_rmV3B_.js → styles-b4e223ce-DWWfWX8O.js} +1 -1
  41. package/dist/assets/{styles-ca3715f6-BFx4VuFc.js → styles-ca3715f6-CKKvZxaU.js} +1 -1
  42. package/dist/assets/{styles-d45a18b0-BE3106vL.js → styles-d45a18b0-dKMOUh9p.js} +1 -1
  43. package/dist/assets/{svgDrawCommon-b86b1483-DwDTO1op.js → svgDrawCommon-b86b1483-CBgjChPM.js} +1 -1
  44. package/dist/assets/{timeline-definition-faaaa080-C4b8qUQZ.js → timeline-definition-faaaa080-NCt-HHmb.js} +1 -1
  45. package/dist/assets/{xychartDiagram-f5964ef8-BRJ9Z4u-.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 -241
  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 +13 -1
  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/NewSessionGuide.scss +35 -13
  86. package/src/components/chat/NewSessionGuide.tsx +20 -10
  87. package/src/components/chat/{Sender.scss → Sender/Sender.scss} +80 -0
  88. package/src/components/chat/{Sender.tsx → Sender/Sender.tsx} +161 -5
  89. package/src/components/chat/tools/DefaultTool.tsx +184 -21
  90. package/src/components/chat/tools/adapter-claude/BashTool.scss +67 -51
  91. package/src/components/chat/tools/adapter-claude/BashTool.tsx +83 -49
  92. package/src/components/chat/tools/adapter-claude/GlobTool.scss +0 -79
  93. package/src/components/chat/tools/adapter-claude/GlobTool.tsx +16 -36
  94. package/src/components/chat/tools/adapter-claude/GrepTool.scss +0 -87
  95. package/src/components/chat/tools/adapter-claude/GrepTool.tsx +22 -41
  96. package/src/components/chat/tools/adapter-claude/LSTool.scss +0 -79
  97. package/src/components/chat/tools/adapter-claude/LSTool.tsx +15 -15
  98. package/src/components/chat/tools/adapter-claude/ReadTool.scss +0 -55
  99. package/src/components/chat/tools/adapter-claude/ReadTool.tsx +20 -42
  100. package/src/components/chat/tools/adapter-claude/TodoTool.scss +8 -23
  101. package/src/components/chat/tools/adapter-claude/TodoTool.tsx +24 -11
  102. package/src/components/chat/tools/adapter-claude/WriteTool.scss +21 -69
  103. package/src/components/chat/tools/adapter-claude/WriteTool.tsx +22 -58
  104. package/src/components/chat/tools/adapter-claude/index.ts +4 -10
  105. package/src/components/chat/tools/adapter-claude/utils.ts +54 -0
  106. package/src/components/chat/tools/core/ToolCallBox.scss +356 -0
  107. package/src/components/chat/{ToolGroup.tsx → tools/core/ToolGroup.tsx} +26 -7
  108. package/src/components/chat/{ToolRenderer.tsx → tools/core/ToolRenderer.tsx} +6 -4
  109. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.scss +11 -0
  110. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +75 -0
  111. package/src/components/chat/tools/plugin-chrome-devtools/index.ts +45 -0
  112. package/src/components/chat/tools/task/GetTaskInfoTool.scss +2 -27
  113. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +48 -38
  114. package/src/components/chat/tools/task/ListTasksTool.scss +3 -28
  115. package/src/components/chat/tools/task/ListTasksTool.tsx +11 -8
  116. package/src/components/chat/tools/task/StartTasksTool.scss +3 -28
  117. package/src/components/chat/tools/task/StartTasksTool.tsx +14 -17
  118. package/src/components/chat/tools/task/components/TaskRow.scss +105 -0
  119. package/src/components/chat/tools/task/components/TaskRow.tsx +163 -0
  120. package/src/components/chat/tools/task/components/TaskToolCard.scss +15 -15
  121. package/src/components/chat/tools/task/components/TaskToolCard.tsx +8 -6
  122. package/src/components/config/AppSettingsPanel.tsx +33 -0
  123. package/src/components/config/ConfigSectionForm.tsx +12 -1
  124. package/src/components/config/channelDefinitions.ts +6 -0
  125. package/src/components/config/configSchema.ts +10 -1
  126. package/src/components/config/recordEditors/ChannelRecordEditor.scss +1 -0
  127. package/src/components/config/recordEditors/ChannelRecordEditor.tsx +397 -0
  128. package/src/components/config/recordEditors/index.tsx +1 -0
  129. package/src/components/knowledge-base/KnowledgeBaseView.tsx +51 -3
  130. package/src/components/knowledge-base/components/RuleItem.tsx +79 -0
  131. package/src/components/knowledge-base/components/RuleList.scss +5 -0
  132. package/src/components/knowledge-base/components/RuleList.tsx +70 -0
  133. package/src/components/knowledge-base/components/RulesTab.tsx +32 -7
  134. package/src/components/knowledge-base/components/SpecItem.tsx +1 -1
  135. package/src/hooks/chat/use-chat-interaction.ts +26 -0
  136. package/src/{components/chat/useChatModels.tsx → hooks/chat/use-chat-models.tsx} +65 -16
  137. package/src/hooks/chat/use-chat-permission-mode.ts +47 -0
  138. package/src/hooks/chat/use-chat-scroll.ts +51 -0
  139. package/src/hooks/chat/use-chat-session-actions.ts +147 -0
  140. package/src/hooks/chat/use-chat-session-messages.ts +250 -0
  141. package/src/hooks/chat/use-chat-session.ts +57 -0
  142. package/src/hooks/chat/use-chat-view.ts +39 -0
  143. package/src/main.tsx +10 -13
  144. package/src/resources/locales/en.json +73 -0
  145. package/src/resources/locales/zh.json +73 -0
  146. package/src/runtime-config.ts +52 -0
  147. package/src/store/index.ts +2 -0
  148. package/src/vite-env.d.ts +11 -0
  149. package/src/ws.ts +5 -3
  150. package/vite.config.ts +12 -4
  151. package/dist/assets/channel-DrWdSpqV.js +0 -1
  152. package/dist/assets/clone-D0cC8LLB.js +0 -1
  153. package/dist/assets/flowDiagram-v2-4f6560a1-Bf_DH7dp.js +0 -1
  154. package/dist/assets/index-CNMzWvKV.js +0 -497
  155. package/dist/assets/index-PEmISxiy.css +0 -1
  156. package/src/components/chat/ToolCallBox.scss +0 -137
  157. package/src/components/chat/useChatSession.ts +0 -370
  158. /package/src/components/{chat/CodeBlock.scss → CodeBlock.scss} +0 -0
  159. /package/src/components/chat/{MessageFooter.tsx → Messages/MessageFooter.tsx} +0 -0
  160. /package/src/components/chat/{CompletionMenu.scss → Sender/CompletionMenu.scss} +0 -0
  161. /package/src/components/chat/{CompletionMenu.tsx → Sender/CompletionMenu.tsx} +0 -0
  162. /package/src/components/chat/{ThinkingStatus.scss → Sender/ThinkingStatus.scss} +0 -0
  163. /package/src/components/chat/{ThinkingStatus.tsx → Sender/ThinkingStatus.tsx} +0 -0
  164. /package/src/components/chat/{ToolCallBox.tsx → tools/core/ToolCallBox.tsx} +0 -0
  165. /package/src/components/chat/{ToolGroup.scss → tools/core/ToolGroup.scss} +0 -0
  166. /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,13 @@ export function Sender({
37
47
  modelOptions,
38
48
  selectedModel,
39
49
  onModelChange,
50
+ permissionMode,
51
+ permissionModeOptions,
52
+ onPermissionModeChange,
40
53
  modelUnavailable
41
54
  }: {
42
55
  onSend: (text: string) => void
56
+ onSendContent: (content: ChatMessageContent[]) => void
43
57
  sessionStatus?: SessionStatus
44
58
  onInterrupt: () => void
45
59
  onClear?: () => void
@@ -50,6 +64,9 @@ export function Sender({
50
64
  modelOptions?: ModelSelectGroup[]
51
65
  selectedModel?: string
52
66
  onModelChange?: (model: string) => void
67
+ permissionMode: PermissionMode
68
+ permissionModeOptions: Array<{ value: PermissionMode; label: React.ReactNode }>
69
+ onPermissionModeChange: (mode: PermissionMode) => void
53
70
  modelUnavailable?: boolean
54
71
  }) {
55
72
  const { t } = useTranslation()
@@ -63,7 +80,9 @@ export function Sender({
63
80
  const [showToolsList, setShowToolsList] = useState(false)
64
81
  const textareaRef = useRef<TextAreaRef>(null)
65
82
  const toolsRef = useRef<HTMLDivElement>(null)
83
+ const fileInputRef = useRef<HTMLInputElement>(null)
66
84
  const isMac = navigator.platform.includes('Mac')
85
+ const [pendingImages, setPendingImages] = useState<PendingImage[]>([])
67
86
 
68
87
  const { data: configRes } = useSWR<{
69
88
  sources?: {
@@ -96,8 +115,54 @@ export function Sender({
96
115
  const [historyIndex, setHistoryIndex] = useState(-1)
97
116
  const [draft, setDraft] = useState('')
98
117
 
118
+ const readFileAsDataUrl = (file: File) => {
119
+ return new Promise<string>((resolve, reject) => {
120
+ const reader = new FileReader()
121
+ reader.onload = () => {
122
+ resolve(typeof reader.result === 'string' ? reader.result : '')
123
+ }
124
+ reader.onerror = () => {
125
+ reject(new Error('read_failed'))
126
+ }
127
+ reader.readAsDataURL(file)
128
+ })
129
+ }
130
+
131
+ const addImageFiles = async (files: File[]) => {
132
+ const maxSize = 5 * 1024 * 1024
133
+ for (const file of files) {
134
+ if (!file.type.startsWith('image/')) continue
135
+ if (file.size > maxSize) {
136
+ void message.error(t('chat.imageTooLarge'))
137
+ continue
138
+ }
139
+ try {
140
+ const url = await readFileAsDataUrl(file)
141
+ if (url === '') {
142
+ void message.error(t('chat.imageReadFailed'))
143
+ continue
144
+ }
145
+ setPendingImages(prev => [
146
+ ...prev,
147
+ {
148
+ id: globalThis.crypto?.randomUUID
149
+ ? globalThis.crypto.randomUUID()
150
+ : `img-${Date.now()}-${Math.random().toString(16).slice(2)}`,
151
+ url,
152
+ name: file.name,
153
+ size: file.size,
154
+ mimeType: file.type
155
+ }
156
+ ])
157
+ } catch (err) {
158
+ void message.error(t('chat.imageReadFailed'))
159
+ }
160
+ }
161
+ }
162
+
99
163
  const handleSend = () => {
100
- if (input.trim() === '' || isThinking) return
164
+ if (isThinking) return
165
+ if (input.trim() === '' && pendingImages.length === 0) return
101
166
 
102
167
  if (modelUnavailable) {
103
168
  void message.warning(t('chat.modelConfigRequired'))
@@ -105,12 +170,31 @@ export function Sender({
105
170
  }
106
171
 
107
172
  if (interactionRequest != null && onInteractionResponse != null) {
173
+ if (pendingImages.length > 0) {
174
+ void message.warning(t('chat.imageNotSupportedInInteraction'))
175
+ return
176
+ }
108
177
  onInteractionResponse(interactionRequest.id, input.trim())
109
178
  setInput('')
110
179
  return
111
180
  }
112
181
 
113
- onSend(input)
182
+ if (pendingImages.length > 0) {
183
+ const content: ChatMessageContent[] = []
184
+ if (input.trim() !== '') {
185
+ content.push({ type: 'text', text: input.trim() })
186
+ }
187
+ content.push(...pendingImages.map(img => ({
188
+ type: 'image',
189
+ url: img.url,
190
+ name: img.name,
191
+ size: img.size,
192
+ mimeType: img.mimeType
193
+ })))
194
+ onSendContent(content)
195
+ } else {
196
+ onSend(input)
197
+ }
114
198
 
115
199
  // Save to local storage history
116
200
  try {
@@ -122,11 +206,48 @@ export function Sender({
122
206
  }
123
207
 
124
208
  setInput('')
209
+ setPendingImages([])
125
210
  setDraft('')
126
211
  setShowCompletion(false)
127
212
  setHistoryIndex(-1)
128
213
  }
129
214
 
215
+ const handleImageUpload = () => {
216
+ if (isThinking) return
217
+ if (modelUnavailable) {
218
+ void message.warning(t('chat.modelConfigRequired'))
219
+ return
220
+ }
221
+ if (interactionRequest != null) {
222
+ void message.warning(t('chat.imageNotSupportedInInteraction'))
223
+ return
224
+ }
225
+ fileInputRef.current?.click()
226
+ }
227
+
228
+ const handleImageFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
229
+ const fileList = Array.from(event.target.files ?? [])
230
+ if (fileList.length === 0) return
231
+ await addImageFiles(fileList)
232
+ event.target.value = ''
233
+ }
234
+
235
+ const handleRemovePendingImage = (id: string) => {
236
+ setPendingImages(prev => prev.filter(img => img.id !== id))
237
+ }
238
+
239
+ const handlePaste = async (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
240
+ const items = Array.from(event.clipboardData?.items ?? [])
241
+ const files = items
242
+ .filter(item => item.kind === 'file' && item.type.startsWith('image/'))
243
+ .map(item => item.getAsFile())
244
+ .filter((file): file is File => file != null)
245
+ if (files.length > 0) {
246
+ event.preventDefault()
247
+ await addImageFiles(files)
248
+ }
249
+ }
250
+
130
251
  const clearInputValue = () => {
131
252
  if (input === '') return
132
253
  setInput('')
@@ -422,6 +543,18 @@ export function Sender({
422
543
  {t('chat.modelConfigRequired')}
423
544
  </div>
424
545
  )}
546
+ {pendingImages.length > 0 && (
547
+ <div className='pending-images'>
548
+ {pendingImages.map(img => (
549
+ <div key={img.id} className='pending-image'>
550
+ <img src={img.url} alt={img.name ?? 'image'} />
551
+ <div className='pending-image-remove' onClick={() => handleRemovePendingImage(img.id)}>
552
+ <span className='material-symbols-rounded'>close</span>
553
+ </div>
554
+ </div>
555
+ ))}
556
+ </div>
557
+ )}
425
558
  {showCompletion && (
426
559
  <CompletionMenu
427
560
  items={completionItems}
@@ -437,12 +570,21 @@ export function Sender({
437
570
  value={input}
438
571
  onChange={handleInputChange}
439
572
  onKeyDown={handleKeyDown}
573
+ onPaste={handlePaste}
440
574
  autoSize={{ minRows: 1, maxRows: 10 }}
441
575
  variant='borderless'
442
576
  disabled={modelUnavailable}
443
577
  />
444
578
 
445
579
  <div className='chat-input-toolbar'>
580
+ <input
581
+ ref={fileInputRef}
582
+ type='file'
583
+ accept='image/*'
584
+ multiple
585
+ onChange={handleImageFileChange}
586
+ className='file-input-hidden'
587
+ />
446
588
  <div className='toolbar-left'>
447
589
  <Tooltip title='快捷指令'>
448
590
  <span>
@@ -467,7 +609,7 @@ export function Sender({
467
609
  </Tooltip>
468
610
  <Tooltip title='上传图片'>
469
611
  <span>
470
- <div className='toolbar-btn' onClick={() => void message.info('图片上传功能尚不支持')}>
612
+ <div className='toolbar-btn' onClick={handleImageUpload}>
471
613
  <span className='material-symbols-rounded'>image</span>
472
614
  </div>
473
615
  </span>
@@ -522,6 +664,20 @@ export function Sender({
522
664
  popupMatchSelectWidth={false}
523
665
  />
524
666
 
667
+ <Select
668
+ className='permission-mode-select'
669
+ classNames={{ popup: { root: 'permission-mode-select-popup' } }}
670
+ value={permissionMode}
671
+ options={permissionModeOptions}
672
+ showSearch={false}
673
+ allowClear={false}
674
+ disabled={modelUnavailable || isThinking}
675
+ onChange={(value) => onPermissionModeChange(value)}
676
+ placeholder='权限模式'
677
+ optionLabelProp='label'
678
+ popupMatchSelectWidth={false}
679
+ />
680
+
525
681
  <div
526
682
  className={`chat-send-btn ${input.trim() !== '' && !modelUnavailable ? 'active' : ''} ${isThinking ? 'thinking' : ''} ${modelUnavailable ? 'disabled' : ''}`}
527
683
  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
  }