@vibe-forge/client 0.2.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/LICENSE +21 -0
  2. package/cli.cjs +6 -0
  3. package/index.html +27 -0
  4. package/package.json +42 -0
  5. package/src/App.tsx +174 -0
  6. package/src/api.ts +241 -0
  7. package/src/components/ArchiveView.scss +168 -0
  8. package/src/components/ArchiveView.tsx +299 -0
  9. package/src/components/AutomationView/AutomationView.scss +26 -0
  10. package/src/components/AutomationView/RuleFormPanel.scss +129 -0
  11. package/src/components/AutomationView/RuleFormPanel.tsx +257 -0
  12. package/src/components/AutomationView/RuleSidebar.scss +219 -0
  13. package/src/components/AutomationView/RuleSidebar.tsx +258 -0
  14. package/src/components/AutomationView/RunHistoryPanel.scss +286 -0
  15. package/src/components/AutomationView/RunHistoryPanel.tsx +320 -0
  16. package/src/components/AutomationView/TaskList.scss +128 -0
  17. package/src/components/AutomationView/TaskList.tsx +79 -0
  18. package/src/components/AutomationView/TriggerList.scss +153 -0
  19. package/src/components/AutomationView/TriggerList.tsx +217 -0
  20. package/src/components/AutomationView/index.tsx +228 -0
  21. package/src/components/AutomationView/types.ts +21 -0
  22. package/src/components/Chat.scss +89 -0
  23. package/src/components/Chat.tsx +92 -0
  24. package/src/components/ConfigView.scss +185 -0
  25. package/src/components/ConfigView.tsx +258 -0
  26. package/src/components/NavRail.scss +71 -0
  27. package/src/components/NavRail.tsx +188 -0
  28. package/src/components/Sidebar.scss +112 -0
  29. package/src/components/Sidebar.tsx +291 -0
  30. package/src/components/chat/ChatHeader.scss +401 -0
  31. package/src/components/chat/ChatHeader.tsx +342 -0
  32. package/src/components/chat/ChatHistoryView.tsx +122 -0
  33. package/src/components/chat/ChatSettingsView.tsx +22 -0
  34. package/src/components/chat/ChatTimelineView.scss +53 -0
  35. package/src/components/chat/ChatTimelineView.tsx +158 -0
  36. package/src/components/chat/CodeBlock.scss +87 -0
  37. package/src/components/chat/CodeBlock.tsx +179 -0
  38. package/src/components/chat/CompletionMenu.scss +70 -0
  39. package/src/components/chat/CompletionMenu.tsx +58 -0
  40. package/src/components/chat/CurrentTodoList.scss +217 -0
  41. package/src/components/chat/CurrentTodoList.tsx +103 -0
  42. package/src/components/chat/MarkdownContent.tsx +43 -0
  43. package/src/components/chat/MessageFooter.tsx +48 -0
  44. package/src/components/chat/MessageItem.scss +251 -0
  45. package/src/components/chat/MessageItem.tsx +78 -0
  46. package/src/components/chat/NewSessionGuide.scss +186 -0
  47. package/src/components/chat/NewSessionGuide.tsx +167 -0
  48. package/src/components/chat/Sender.scss +367 -0
  49. package/src/components/chat/Sender.tsx +541 -0
  50. package/src/components/chat/SessionTimelinePanel/EventList.scss +58 -0
  51. package/src/components/chat/SessionTimelinePanel/EventList.tsx +212 -0
  52. package/src/components/chat/SessionTimelinePanel/gantt.ts +177 -0
  53. package/src/components/chat/SessionTimelinePanel/git-graph.ts +518 -0
  54. package/src/components/chat/SessionTimelinePanel/index.scss +28 -0
  55. package/src/components/chat/SessionTimelinePanel/index.tsx +121 -0
  56. package/src/components/chat/SessionTimelinePanel/mermaid.ts +4 -0
  57. package/src/components/chat/SessionTimelinePanel/types.ts +64 -0
  58. package/src/components/chat/SessionTimelinePanel/utils.ts +20 -0
  59. package/src/components/chat/ThinkingStatus.scss +70 -0
  60. package/src/components/chat/ThinkingStatus.tsx +13 -0
  61. package/src/components/chat/ToolCallBox.scss +137 -0
  62. package/src/components/chat/ToolCallBox.tsx +55 -0
  63. package/src/components/chat/ToolGroup.scss +154 -0
  64. package/src/components/chat/ToolGroup.tsx +102 -0
  65. package/src/components/chat/ToolRenderer.tsx +45 -0
  66. package/src/components/chat/messageUtils.ts +171 -0
  67. package/src/components/chat/safeSerialize.ts +84 -0
  68. package/src/components/chat/tools/DefaultTool.tsx +63 -0
  69. package/src/components/chat/tools/adapter-claude/BashTool.scss +71 -0
  70. package/src/components/chat/tools/adapter-claude/BashTool.tsx +82 -0
  71. package/src/components/chat/tools/adapter-claude/GlobTool.scss +88 -0
  72. package/src/components/chat/tools/adapter-claude/GlobTool.tsx +85 -0
  73. package/src/components/chat/tools/adapter-claude/GrepTool.scss +96 -0
  74. package/src/components/chat/tools/adapter-claude/GrepTool.tsx +114 -0
  75. package/src/components/chat/tools/adapter-claude/LSTool.scss +85 -0
  76. package/src/components/chat/tools/adapter-claude/LSTool.tsx +94 -0
  77. package/src/components/chat/tools/adapter-claude/ReadTool.scss +57 -0
  78. package/src/components/chat/tools/adapter-claude/ReadTool.tsx +87 -0
  79. package/src/components/chat/tools/adapter-claude/TodoTool.scss +78 -0
  80. package/src/components/chat/tools/adapter-claude/TodoTool.tsx +60 -0
  81. package/src/components/chat/tools/adapter-claude/WriteTool.scss +92 -0
  82. package/src/components/chat/tools/adapter-claude/WriteTool.tsx +86 -0
  83. package/src/components/chat/tools/adapter-claude/components/FileList.scss +65 -0
  84. package/src/components/chat/tools/adapter-claude/components/FileList.tsx +185 -0
  85. package/src/components/chat/tools/adapter-claude/index.ts +28 -0
  86. package/src/components/chat/tools/defineToolRender.ts +28 -0
  87. package/src/components/chat/tools/task/GetTaskInfoTool.scss +50 -0
  88. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +88 -0
  89. package/src/components/chat/tools/task/ListTasksTool.scss +56 -0
  90. package/src/components/chat/tools/task/ListTasksTool.tsx +83 -0
  91. package/src/components/chat/tools/task/StartTasksTool.scss +56 -0
  92. package/src/components/chat/tools/task/StartTasksTool.tsx +96 -0
  93. package/src/components/chat/tools/task/components/TaskToolCard.scss +127 -0
  94. package/src/components/chat/tools/task/components/TaskToolCard.tsx +177 -0
  95. package/src/components/chat/tools/task/index.ts +15 -0
  96. package/src/components/chat/useChatModels.tsx +206 -0
  97. package/src/components/chat/useChatSession.ts +370 -0
  98. package/src/components/config/ConfigAboutSection.scss +111 -0
  99. package/src/components/config/ConfigAboutSection.tsx +86 -0
  100. package/src/components/config/ConfigDisplayValue.scss +22 -0
  101. package/src/components/config/ConfigDisplayValue.tsx +62 -0
  102. package/src/components/config/ConfigEditors.scss +65 -0
  103. package/src/components/config/ConfigEditors.tsx +98 -0
  104. package/src/components/config/ConfigFieldRow.scss +97 -0
  105. package/src/components/config/ConfigFieldRow.tsx +36 -0
  106. package/src/components/config/ConfigSectionForm.scss +94 -0
  107. package/src/components/config/ConfigSectionForm.tsx +436 -0
  108. package/src/components/config/ConfigSectionPanel.tsx +67 -0
  109. package/src/components/config/ConfigShortcutInput.scss +11 -0
  110. package/src/components/config/ConfigShortcutInput.tsx +52 -0
  111. package/src/components/config/ConfigSourceSwitch.tsx +57 -0
  112. package/src/components/config/configSchema.ts +319 -0
  113. package/src/components/config/configUtils.ts +83 -0
  114. package/src/components/config/index.tsx +5 -0
  115. package/src/components/config/recordEditors/BooleanRecordEditor.scss +1 -0
  116. package/src/components/config/recordEditors/BooleanRecordEditor.tsx +75 -0
  117. package/src/components/config/recordEditors/KeyValueEditor.scss +1 -0
  118. package/src/components/config/recordEditors/KeyValueEditor.tsx +97 -0
  119. package/src/components/config/recordEditors/McpServersRecordEditor.scss +1 -0
  120. package/src/components/config/recordEditors/McpServersRecordEditor.tsx +258 -0
  121. package/src/components/config/recordEditors/ModelServicesRecordEditor.scss +1 -0
  122. package/src/components/config/recordEditors/ModelServicesRecordEditor.tsx +233 -0
  123. package/src/components/config/recordEditors/RecordEditors.scss +117 -0
  124. package/src/components/config/recordEditors/RecordJsonEditor.scss +1 -0
  125. package/src/components/config/recordEditors/RecordJsonEditor.tsx +113 -0
  126. package/src/components/config/recordEditors/index.tsx +5 -0
  127. package/src/components/knowledge-base/KnowledgeBaseView.scss +19 -0
  128. package/src/components/knowledge-base/KnowledgeBaseView.tsx +186 -0
  129. package/src/components/knowledge-base/components/ActionButton.scss +5 -0
  130. package/src/components/knowledge-base/components/ActionButton.tsx +9 -0
  131. package/src/components/knowledge-base/components/EmptyState.scss +19 -0
  132. package/src/components/knowledge-base/components/EmptyState.tsx +42 -0
  133. package/src/components/knowledge-base/components/EntitiesTab.scss +5 -0
  134. package/src/components/knowledge-base/components/EntitiesTab.tsx +80 -0
  135. package/src/components/knowledge-base/components/EntityItem.scss +82 -0
  136. package/src/components/knowledge-base/components/EntityItem.tsx +79 -0
  137. package/src/components/knowledge-base/components/EntityList.scss +5 -0
  138. package/src/components/knowledge-base/components/EntityList.tsx +70 -0
  139. package/src/components/knowledge-base/components/FilterBar.scss +21 -0
  140. package/src/components/knowledge-base/components/FilterBar.tsx +51 -0
  141. package/src/components/knowledge-base/components/FlowsTab.scss +5 -0
  142. package/src/components/knowledge-base/components/FlowsTab.tsx +80 -0
  143. package/src/components/knowledge-base/components/KnowledgeBaseHeader.scss +27 -0
  144. package/src/components/knowledge-base/components/KnowledgeBaseHeader.tsx +29 -0
  145. package/src/components/knowledge-base/components/KnowledgeList.scss +19 -0
  146. package/src/components/knowledge-base/components/KnowledgeList.tsx +19 -0
  147. package/src/components/knowledge-base/components/LoadingState.scss +5 -0
  148. package/src/components/knowledge-base/components/LoadingState.tsx +11 -0
  149. package/src/components/knowledge-base/components/MetaList.scss +19 -0
  150. package/src/components/knowledge-base/components/MetaList.tsx +18 -0
  151. package/src/components/knowledge-base/components/RulesTab.scss +5 -0
  152. package/src/components/knowledge-base/components/RulesTab.tsx +49 -0
  153. package/src/components/knowledge-base/components/SectionHeader.scss +22 -0
  154. package/src/components/knowledge-base/components/SectionHeader.tsx +21 -0
  155. package/src/components/knowledge-base/components/SkillsTab.scss +5 -0
  156. package/src/components/knowledge-base/components/SkillsTab.tsx +49 -0
  157. package/src/components/knowledge-base/components/SpecItem.scss +138 -0
  158. package/src/components/knowledge-base/components/SpecItem.tsx +131 -0
  159. package/src/components/knowledge-base/components/SpecList.scss +5 -0
  160. package/src/components/knowledge-base/components/SpecList.tsx +70 -0
  161. package/src/components/knowledge-base/components/TabContent.scss +8 -0
  162. package/src/components/knowledge-base/components/TabContent.tsx +17 -0
  163. package/src/components/knowledge-base/components/TabLabel.scss +10 -0
  164. package/src/components/knowledge-base/components/TabLabel.tsx +15 -0
  165. package/src/components/knowledge-base/index.tsx +1 -0
  166. package/src/components/sidebar/SessionItem.scss +256 -0
  167. package/src/components/sidebar/SessionItem.tsx +265 -0
  168. package/src/components/sidebar/SessionList.scss +92 -0
  169. package/src/components/sidebar/SessionList.tsx +166 -0
  170. package/src/components/sidebar/SidebarHeader.scss +79 -0
  171. package/src/components/sidebar/SidebarHeader.tsx +128 -0
  172. package/src/connectionManager.ts +172 -0
  173. package/src/hooks/useGlobalShortcut.ts +26 -0
  174. package/src/hooks/useQueryParams.ts +54 -0
  175. package/src/i18n.ts +22 -0
  176. package/src/main.tsx +41 -0
  177. package/src/resources/locales/en.json +765 -0
  178. package/src/resources/locales/zh.json +766 -0
  179. package/src/store/index.ts +23 -0
  180. package/src/styles/global.scss +100 -0
  181. package/src/utils/shortcutUtils.ts +88 -0
  182. package/src/vite-env.d.ts +12 -0
  183. package/src/ws.ts +33 -0
  184. package/vite.config.ts +26 -0
@@ -0,0 +1,541 @@
1
+ import './Sender.scss'
2
+
3
+ import { App, Button, Input, Select, Tooltip } from 'antd'
4
+ import type { TextAreaRef } from 'antd/es/input/TextArea'
5
+ import React, { useEffect, useRef, useState } from 'react'
6
+ import { useTranslation } from 'react-i18next'
7
+ import useSWR from 'swr'
8
+
9
+ import type { AskUserQuestionParams, SessionInfo, SessionStatus } from '@vibe-forge/core'
10
+ import type { CompletionItem } from './CompletionMenu'
11
+ import { CompletionMenu } from './CompletionMenu'
12
+ import { ThinkingStatus } from './ThinkingStatus'
13
+ import { isShortcutMatch } from '../../utils/shortcutUtils'
14
+
15
+ const { TextArea } = Input
16
+
17
+ interface ModelSelectOption {
18
+ value: string
19
+ label: React.ReactNode
20
+ searchText: string
21
+ }
22
+
23
+ interface ModelSelectGroup {
24
+ label: React.ReactNode
25
+ options: ModelSelectOption[]
26
+ }
27
+
28
+ export function Sender({
29
+ onSend,
30
+ sessionStatus,
31
+ onInterrupt,
32
+ onClear,
33
+ sessionInfo,
34
+ interactionRequest,
35
+ onInteractionResponse,
36
+ placeholder,
37
+ modelOptions,
38
+ selectedModel,
39
+ onModelChange,
40
+ modelUnavailable
41
+ }: {
42
+ onSend: (text: string) => void
43
+ sessionStatus?: SessionStatus
44
+ onInterrupt: () => void
45
+ onClear?: () => void
46
+ sessionInfo?: SessionInfo | null
47
+ interactionRequest?: { id: string; payload: AskUserQuestionParams } | null
48
+ onInteractionResponse?: (id: string, data: string | string[]) => void
49
+ placeholder?: string
50
+ modelOptions?: ModelSelectGroup[]
51
+ selectedModel?: string
52
+ onModelChange?: (model: string) => void
53
+ modelUnavailable?: boolean
54
+ }) {
55
+ const { t } = useTranslation()
56
+ const { message } = App.useApp()
57
+ const [input, setInput] = useState('')
58
+ const [showCompletion, setShowCompletion] = useState(false)
59
+ const [completionItems, setCompletionItems] = useState<CompletionItem[]>([])
60
+ const [selectedIndex, setSelectedIndex] = useState(0)
61
+ const [triggerChar, setTriggerChar] = useState<string | null>(null)
62
+
63
+ const [showToolsList, setShowToolsList] = useState(false)
64
+ const textareaRef = useRef<TextAreaRef>(null)
65
+ const toolsRef = useRef<HTMLDivElement>(null)
66
+ const isMac = navigator.platform.includes('Mac')
67
+
68
+ const { data: configRes } = useSWR<{
69
+ sources?: {
70
+ merged?: {
71
+ shortcuts?: {
72
+ sendMessage?: string
73
+ clearInput?: string
74
+ }
75
+ }
76
+ }
77
+ }>('/api/config')
78
+ const sendShortcut = configRes?.sources?.merged?.shortcuts?.sendMessage
79
+ const clearInputShortcut = configRes?.sources?.merged?.shortcuts?.clearInput
80
+ const resolvedSendShortcut = sendShortcut != null && sendShortcut.trim() !== ''
81
+ ? sendShortcut
82
+ : 'mod+enter'
83
+
84
+ const isThinking = sessionStatus === 'running'
85
+
86
+ useEffect(() => {
87
+ const handleClickOutside = (event: MouseEvent) => {
88
+ if (toolsRef.current && !toolsRef.current.contains(event.target as Node)) {
89
+ setShowToolsList(false)
90
+ }
91
+ }
92
+ document.addEventListener('mousedown', handleClickOutside)
93
+ return () => document.removeEventListener('mousedown', handleClickOutside)
94
+ }, [])
95
+
96
+ const [historyIndex, setHistoryIndex] = useState(-1)
97
+ const [draft, setDraft] = useState('')
98
+
99
+ const handleSend = () => {
100
+ if (input.trim() === '' || isThinking) return
101
+
102
+ if (modelUnavailable) {
103
+ void message.warning(t('chat.modelConfigRequired'))
104
+ return
105
+ }
106
+
107
+ if (interactionRequest != null && onInteractionResponse != null) {
108
+ onInteractionResponse(interactionRequest.id, input.trim())
109
+ setInput('')
110
+ return
111
+ }
112
+
113
+ onSend(input)
114
+
115
+ // Save to local storage history
116
+ try {
117
+ const history = JSON.parse(localStorage.getItem('vf_chat_history') ?? '[]') as string[]
118
+ const newHistory = [input, ...history.filter((h: string) => h !== input)].slice(0, 50)
119
+ localStorage.setItem('vf_chat_history', JSON.stringify(newHistory))
120
+ } catch (e) {
121
+ console.error('Failed to save chat history', e)
122
+ }
123
+
124
+ setInput('')
125
+ setDraft('')
126
+ setShowCompletion(false)
127
+ setHistoryIndex(-1)
128
+ }
129
+
130
+ const clearInputValue = () => {
131
+ if (input === '') return
132
+ setInput('')
133
+ setHistoryIndex(-1)
134
+ }
135
+
136
+ const handleHistoryNavigation = (direction: 'up' | 'down') => {
137
+ try {
138
+ const history = JSON.parse(localStorage.getItem('vf_chat_history') ?? '[]') as string[]
139
+ if (history.length === 0) return
140
+
141
+ let nextIndex = historyIndex
142
+ if (direction === 'up') {
143
+ nextIndex = Math.min(historyIndex + 1, history.length - 1)
144
+ } else {
145
+ nextIndex = Math.max(historyIndex - 1, -1)
146
+ }
147
+
148
+ if (nextIndex !== historyIndex) {
149
+ // Save draft when leaving -1
150
+ if (historyIndex === -1) {
151
+ setDraft(input)
152
+ }
153
+
154
+ setHistoryIndex(nextIndex)
155
+ const nextValue = nextIndex === -1 ? draft : history[nextIndex]
156
+ setInput(nextValue)
157
+
158
+ // Set cursor to the end of the text
159
+ setTimeout(() => {
160
+ if (textareaRef.current?.resizableTextArea?.textArea != null) {
161
+ const textArea = textareaRef.current.resizableTextArea.textArea
162
+ const length = nextValue.length
163
+ textArea.setSelectionRange(length, length)
164
+ textArea.focus()
165
+ }
166
+ }, 0)
167
+ }
168
+ } catch (e) {
169
+ console.error('Failed to navigate chat history', e)
170
+ }
171
+ }
172
+
173
+ const handleSelectCompletion = (item: CompletionItem) => {
174
+ if (triggerChar == null || textareaRef.current?.resizableTextArea?.textArea == null) return
175
+
176
+ const textArea = textareaRef.current.resizableTextArea.textArea
177
+ const cursorFallback = textArea.selectionStart
178
+ const textBeforeTrigger = input.slice(0, input.lastIndexOf(triggerChar, cursorFallback - 1))
179
+ const textAfterCursor = input.slice(cursorFallback)
180
+
181
+ const newValue = `${textBeforeTrigger}${triggerChar}${item.value} ${textAfterCursor}`
182
+ setInput(newValue)
183
+ setShowCompletion(false)
184
+
185
+ // Focus back and set cursor
186
+ setTimeout(() => {
187
+ if (textareaRef.current?.resizableTextArea?.textArea != null) {
188
+ const textArea = textareaRef.current.resizableTextArea.textArea
189
+ const newCursorPos = textBeforeTrigger.length + triggerChar.length + item.value.length + 1
190
+ textArea.focus()
191
+ textArea.setSelectionRange(newCursorPos, newCursorPos)
192
+ }
193
+ }, 0)
194
+ }
195
+
196
+ const handleTriggerClick = (char: string) => {
197
+ if (textareaRef.current?.resizableTextArea?.textArea == null) return
198
+ const textArea = textareaRef.current.resizableTextArea.textArea
199
+ const cursor = textArea.selectionStart
200
+ const textBefore = input.slice(0, cursor)
201
+ const textAfter = input.slice(cursor)
202
+
203
+ // Check if we need to add a space before the trigger char
204
+ const needsSpaceBefore = textBefore.length > 0 && !textBefore.endsWith(' ')
205
+ const trigger = needsSpaceBefore ? ` ${char}` : char
206
+
207
+ const newValue = textBefore + trigger + textAfter
208
+ setInput(newValue)
209
+
210
+ setTimeout(() => {
211
+ if (textareaRef.current?.resizableTextArea?.textArea != null) {
212
+ const textArea = textareaRef.current.resizableTextArea.textArea
213
+ const newPos = cursor + trigger.length
214
+ textArea.focus()
215
+ textArea.setSelectionRange(newPos, newPos)
216
+
217
+ // Trigger handleInputChange logic manually
218
+ const event = { target: textArea } as unknown as React.ChangeEvent<HTMLTextAreaElement>
219
+ handleInputChange(event)
220
+ }
221
+ }, 0)
222
+ }
223
+
224
+ const handleKeyDown = (e: React.KeyboardEvent) => {
225
+ if (isShortcutMatch(e, resolvedSendShortcut, isMac)) {
226
+ e.preventDefault()
227
+ handleSend()
228
+ return
229
+ }
230
+ if (clearInputShortcut != null && clearInputShortcut.trim() !== '' && isShortcutMatch(e, clearInputShortcut, isMac)) {
231
+ e.preventDefault()
232
+ clearInputValue()
233
+ return
234
+ }
235
+ if (showCompletion) {
236
+ if (e.key === 'ArrowDown') {
237
+ e.preventDefault()
238
+ setSelectedIndex(prev => (prev + 1) % completionItems.length)
239
+ return
240
+ }
241
+ if (e.key === 'ArrowUp') {
242
+ e.preventDefault()
243
+ setSelectedIndex(prev => (prev - 1 + completionItems.length) % completionItems.length)
244
+ return
245
+ }
246
+ if (e.key === 'Enter' || e.key === 'Tab') {
247
+ e.preventDefault()
248
+ const selectedItem = completionItems[selectedIndex]
249
+ if (selectedItem != null) {
250
+ handleSelectCompletion(selectedItem)
251
+ }
252
+ return
253
+ }
254
+ if (e.key === 'Escape') {
255
+ e.preventDefault()
256
+ setShowCompletion(false)
257
+ return
258
+ }
259
+ }
260
+
261
+ // History navigation logic
262
+ if (e.key === 'ArrowUp') {
263
+ const textarea = e.target as HTMLTextAreaElement
264
+ const cursorPosition = textarea.selectionStart
265
+ const textBeforeCursor = textarea.value.substring(0, cursorPosition)
266
+
267
+ // Only navigate if cursor is at the first line
268
+ if (!textBeforeCursor.includes('\n')) {
269
+ const historyJson = localStorage.getItem('vf_chat_history')
270
+ const history = (historyJson != null ? JSON.parse(historyJson) : []) as string[]
271
+ const currentHistoryValue = historyIndex === -1 ? null : history[historyIndex]
272
+
273
+ // If content is empty OR content matches the current history entry, allow navigation
274
+ if (input.trim() === '' || input === currentHistoryValue) {
275
+ e.preventDefault()
276
+ handleHistoryNavigation('up')
277
+ return
278
+ }
279
+ }
280
+ }
281
+
282
+ if (e.key === 'ArrowDown') {
283
+ const textarea = e.target as HTMLTextAreaElement
284
+ const cursorPosition = textarea.selectionEnd
285
+ const textAfterCursor = textarea.value.substring(cursorPosition)
286
+
287
+ // Only navigate if cursor is at the last line
288
+ if (!textAfterCursor.includes('\n')) {
289
+ const historyJson = localStorage.getItem('vf_chat_history')
290
+ const history = (historyJson != null ? JSON.parse(historyJson) : []) as string[]
291
+ const currentHistoryValue = historyIndex === -1 ? null : history[historyIndex]
292
+
293
+ // If history navigation has started (index >= 0) OR content matches current history entry
294
+ if (historyIndex !== -1 || input === currentHistoryValue) {
295
+ e.preventDefault()
296
+ handleHistoryNavigation('down')
297
+ return
298
+ }
299
+ }
300
+ }
301
+
302
+ // More shortcuts
303
+ if (e.key === 'Escape') {
304
+ if (input !== '') {
305
+ e.preventDefault()
306
+ clearInputValue()
307
+ }
308
+ return
309
+ }
310
+
311
+ // Cmd/Ctrl + L to clear screen
312
+ if (e.key === 'l' && (e.metaKey || e.ctrlKey)) {
313
+ e.preventDefault()
314
+ setInput('')
315
+ setHistoryIndex(-1)
316
+ if (onClear != null) {
317
+ onClear()
318
+ } else {
319
+ void message.info('Clear screen is not supported in this context')
320
+ }
321
+ return
322
+ }
323
+
324
+ // Cmd/Ctrl + Enter to send
325
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
326
+ e.preventDefault()
327
+ handleSend()
328
+ }
329
+ }
330
+
331
+ const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
332
+ const value = e.target.value
333
+ setInput(value)
334
+
335
+ const cursor = e.target.selectionStart
336
+ const charBeforeCursor = value[cursor - 1]
337
+
338
+ if (['/', '@', '#'].includes(charBeforeCursor)) {
339
+ setTriggerChar(charBeforeCursor)
340
+ let items: CompletionItem[] = []
341
+
342
+ if (sessionInfo?.type === 'init') {
343
+ const info = sessionInfo
344
+ if (charBeforeCursor === '/') {
345
+ items = (info.slashCommands != null ? info.slashCommands : []).map(cmd => ({
346
+ label: `/${cmd}`,
347
+ value: cmd,
348
+ icon: 'terminal'
349
+ }))
350
+ } else if (charBeforeCursor === '@') {
351
+ items = (info.agents != null ? info.agents : []).map(agent => ({
352
+ label: `@${agent}`,
353
+ value: agent,
354
+ icon: 'smart_toy'
355
+ }))
356
+ } else if (charBeforeCursor === '#') {
357
+ items = (info.tools != null ? info.tools : []).map(tool => ({
358
+ label: `#${tool}`,
359
+ value: tool,
360
+ icon: 'check_box'
361
+ }))
362
+ }
363
+ }
364
+
365
+ if (items.length > 0) {
366
+ setCompletionItems(items)
367
+ setSelectedIndex(0)
368
+ setShowCompletion(true)
369
+ } else {
370
+ setShowCompletion(false)
371
+ }
372
+ } else if (showCompletion) {
373
+ // Filter logic could go here if needed
374
+ if (!value.includes(triggerChar ?? '')) {
375
+ setShowCompletion(false)
376
+ }
377
+ }
378
+ }
379
+
380
+ return (
381
+ <div className='chat-input-wrapper'>
382
+ {isThinking && <ThinkingStatus />}
383
+ {interactionRequest != null && (
384
+ <div
385
+ className='interaction-panel'
386
+ style={{
387
+ display: 'flex',
388
+ flexDirection: 'column',
389
+ maxHeight: '200px',
390
+ overflowY: 'auto',
391
+ marginBottom: '10px',
392
+ gap: '8px',
393
+ padding: '8px',
394
+ border: '1px solid var(--border-color)',
395
+ borderRadius: '8px',
396
+ backgroundColor: 'var(--bg-color)'
397
+ }}
398
+ >
399
+ <div className='interaction-question' style={{ fontWeight: 'bold' }}>
400
+ {interactionRequest.payload.question}
401
+ </div>
402
+ {interactionRequest.payload.options?.map((option) => (
403
+ <Button
404
+ key={option.label}
405
+ block
406
+ style={{ height: 'auto', textAlign: 'left', display: 'block', padding: '8px 12px' }}
407
+ onClick={() => onInteractionResponse?.(interactionRequest.id, option.label)}
408
+ >
409
+ <div style={{ fontWeight: 500 }}>{option.label}</div>
410
+ {option.description && (
411
+ <div style={{ fontSize: '12px', color: 'var(--sub-text-color)', marginTop: '4px' }}>
412
+ {option.description}
413
+ </div>
414
+ )}
415
+ </Button>
416
+ ))}
417
+ </div>
418
+ )}
419
+ <div className='chat-input-container'>
420
+ {modelUnavailable && (
421
+ <div className='model-unavailable'>
422
+ {t('chat.modelConfigRequired')}
423
+ </div>
424
+ )}
425
+ {showCompletion && (
426
+ <CompletionMenu
427
+ items={completionItems}
428
+ selectedIndex={selectedIndex}
429
+ onSelect={handleSelectCompletion}
430
+ onClose={() => setShowCompletion(false)}
431
+ />
432
+ )}
433
+ <TextArea
434
+ ref={textareaRef}
435
+ className='chat-input-textarea'
436
+ placeholder={placeholder ?? interactionRequest?.payload.question ?? t('chat.inputPlaceholder')}
437
+ value={input}
438
+ onChange={handleInputChange}
439
+ onKeyDown={handleKeyDown}
440
+ autoSize={{ minRows: 1, maxRows: 10 }}
441
+ variant='borderless'
442
+ disabled={modelUnavailable}
443
+ />
444
+
445
+ <div className='chat-input-toolbar'>
446
+ <div className='toolbar-left'>
447
+ <Tooltip title='快捷指令'>
448
+ <span>
449
+ <div className='toolbar-btn' onClick={() => handleTriggerClick('/')}>
450
+ <span className='material-symbols-rounded'>terminal</span>
451
+ </div>
452
+ </span>
453
+ </Tooltip>
454
+ <Tooltip title='提及代理'>
455
+ <span>
456
+ <div className='toolbar-btn' onClick={() => handleTriggerClick('@')}>
457
+ <span className='material-symbols-rounded'>smart_toy</span>
458
+ </div>
459
+ </span>
460
+ </Tooltip>
461
+ <Tooltip title='注入上下文'>
462
+ <span>
463
+ <div className='toolbar-btn' onClick={() => handleTriggerClick('#')}>
464
+ <span className='material-symbols-rounded'>description</span>
465
+ </div>
466
+ </span>
467
+ </Tooltip>
468
+ <Tooltip title='上传图片'>
469
+ <span>
470
+ <div className='toolbar-btn' onClick={() => void message.info('图片上传功能尚不支持')}>
471
+ <span className='material-symbols-rounded'>image</span>
472
+ </div>
473
+ </span>
474
+ </Tooltip>
475
+
476
+ {sessionInfo != null && sessionInfo.type === 'init' && (
477
+ <div className='session-info-toolbar' ref={toolsRef}>
478
+ <div
479
+ className={`info-item ${showToolsList ? 'active' : ''}`}
480
+ onClick={() => setShowToolsList(!showToolsList)}
481
+ >
482
+ <span className='material-symbols-rounded'>build</span>
483
+ <span className='info-text'>{t('chat.toolsCount', { count: sessionInfo.tools.length })}</span>
484
+ <span className='material-symbols-rounded arrow-icon'>keyboard_arrow_up</span>
485
+ </div>
486
+
487
+ {showToolsList && (
488
+ <div className='tools-list-popup'>
489
+ <div className='popup-header'>{t('chat.availableTools')}</div>
490
+ <div className='popup-content'>
491
+ <div className='tools-list'>
492
+ {sessionInfo.tools.map(tool => (
493
+ <div key={tool} className='tool-item'>
494
+ <span className='material-symbols-rounded'>check_circle</span>
495
+ <span className='tool-name'>{tool}</span>
496
+ </div>
497
+ ))}
498
+ </div>
499
+ </div>
500
+ </div>
501
+ )}
502
+ </div>
503
+ )}
504
+ </div>
505
+
506
+ <div className='toolbar-right'>
507
+ <Select
508
+ className='model-select'
509
+ classNames={{ popup: { root: 'model-select-popup' } }}
510
+ value={selectedModel}
511
+ options={modelOptions ?? []}
512
+ showSearch
513
+ allowClear={false}
514
+ disabled={modelUnavailable || isThinking}
515
+ onChange={(value) => onModelChange?.(value)}
516
+ placeholder={modelUnavailable ? t('chat.modelUnavailable') : t('chat.modelSelectPlaceholder')}
517
+ optionLabelProp='value'
518
+ filterOption={(input, option) => {
519
+ const searchText = String((option as ModelSelectOption | undefined)?.searchText ?? '')
520
+ return searchText.toLowerCase().includes(input.toLowerCase())
521
+ }}
522
+ popupMatchSelectWidth={false}
523
+ />
524
+
525
+ <div
526
+ className={`chat-send-btn ${input.trim() !== '' && !modelUnavailable ? 'active' : ''} ${isThinking ? 'thinking' : ''} ${modelUnavailable ? 'disabled' : ''}`}
527
+ onClick={modelUnavailable ? undefined : (isThinking ? onInterrupt : handleSend)}
528
+ >
529
+ <span className='material-symbols-rounded'>
530
+ {isThinking ? 'stop_circle' : 'send'}
531
+ </span>
532
+ </div>
533
+ </div>
534
+ </div>
535
+ </div>
536
+ <div className='chat-input-hint'>
537
+ {t('chat.hint')}
538
+ </div>
539
+ </div>
540
+ )
541
+ }
@@ -0,0 +1,58 @@
1
+ .session-timeline-event-table {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 12px;
5
+ padding-top: 12px;
6
+ height: 100%;
7
+ min-height: 0;
8
+ }
9
+
10
+ .session-timeline-event-table__header {
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: space-between;
14
+ gap: 12px;
15
+ }
16
+
17
+ .session-timeline-event-table__title {
18
+ font-size: 13px;
19
+ font-weight: 600;
20
+ color: var(--text-color);
21
+ }
22
+
23
+ .session-timeline-event-table__search {
24
+ width: 240px;
25
+ max-width: 40%;
26
+ }
27
+
28
+ .session-timeline-event-table__table {
29
+ flex: 1 1 auto;
30
+ min-height: 0;
31
+ }
32
+
33
+ .session-timeline-event-table__table .ant-table {
34
+ height: 100%;
35
+ }
36
+
37
+ .session-timeline-event-table__table .ant-table-container {
38
+ height: 100%;
39
+ }
40
+
41
+ .session-timeline-event-table__table .ant-table-body {
42
+ overflow-y: auto;
43
+ }
44
+
45
+ .session-timeline-event-table__time {
46
+ color: var(--secondary-text-color);
47
+ font-variant-numeric: tabular-nums;
48
+ }
49
+
50
+ .session-timeline-event-table__label {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ color: var(--text-color);
54
+ }
55
+
56
+ .session-timeline-event-table__value {
57
+ color: var(--sub-text-color);
58
+ }