@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,171 @@
1
+ import type { ChatMessage, ChatMessageContent } from '@vibe-forge/core'
2
+
3
+ export interface ToolGroupItem {
4
+ type: 'tool-group'
5
+ id: string
6
+ items: {
7
+ item: Extract<ChatMessageContent, { type: 'tool_use' }>
8
+ resultItem?: Extract<ChatMessageContent, { type: 'tool_result' }>
9
+ }[]
10
+ footer?: {
11
+ model?: string
12
+ usage?: ChatMessage['usage']
13
+ createdAt: number
14
+ originalMessage: ChatMessage
15
+ }
16
+ }
17
+
18
+ export interface MessageRenderItem {
19
+ type: 'message'
20
+ message: ChatMessage
21
+ isFirstInGroup: boolean
22
+ }
23
+
24
+ export type ChatRenderItem = MessageRenderItem | ToolGroupItem
25
+
26
+ export function processMessages(messages: ChatMessage[]): ChatRenderItem[] {
27
+ const result: ChatRenderItem[] = []
28
+
29
+ const findResult = (
30
+ toolId: string,
31
+ startIndex: number,
32
+ allMsgs: ChatMessage[]
33
+ ): Extract<ChatMessageContent, { type: 'tool_result' }> | undefined => {
34
+ for (let i = startIndex; i < allMsgs.length; i++) {
35
+ const msg = allMsgs[i]
36
+ if (Array.isArray(msg.content)) {
37
+ const found = msg.content.find(
38
+ c => c.type === 'tool_result' && c.tool_use_id === toolId
39
+ )
40
+ if (found) {
41
+ return found as Extract<ChatMessageContent, { type: 'tool_result' }>
42
+ }
43
+ }
44
+ }
45
+ return undefined
46
+ }
47
+
48
+ let i = 0
49
+ while (i < messages.length) {
50
+ const msg = messages[i]
51
+ if (!msg || !msg.content) {
52
+ // If content is null/empty but legacy toolCall exists, we should render it
53
+ if (msg && msg.toolCall) {
54
+ result.push({
55
+ type: 'message',
56
+ message: msg,
57
+ isFirstInGroup: i === 0 || (i > 0 && messages[i - 1]?.role !== msg.role)
58
+ })
59
+ }
60
+ i++
61
+ continue
62
+ }
63
+
64
+ const prevMsg = i > 0 ? messages[i - 1] : null
65
+ const isFirstInGroup = i === 0 || (prevMsg != null && prevMsg.role !== msg.role)
66
+
67
+ if (typeof msg.content === 'string') {
68
+ result.push({
69
+ type: 'message',
70
+ message: msg,
71
+ isFirstInGroup
72
+ })
73
+ i++
74
+ continue
75
+ }
76
+
77
+ if (Array.isArray(msg.content)) {
78
+ const content = msg.content
79
+ const textParts: ChatMessageContent[] = []
80
+ let currentToolGroup: {
81
+ item: Extract<ChatMessageContent, { type: 'tool_use' }>
82
+ resultItem?: Extract<ChatMessageContent, { type: 'tool_result' }>
83
+ }[] = []
84
+
85
+ let producedCount = 0
86
+
87
+ const flushTools = () => {
88
+ if (currentToolGroup.length > 0) {
89
+ result.push({
90
+ type: 'tool-group',
91
+ id: `group-${msg.id}-${producedCount}`,
92
+ items: [...currentToolGroup],
93
+ footer: undefined
94
+ })
95
+ currentToolGroup = []
96
+ producedCount++
97
+ }
98
+ }
99
+
100
+ const flushText = () => {
101
+ if (textParts.length > 0) {
102
+ result.push({
103
+ type: 'message',
104
+ message: {
105
+ ...msg,
106
+ id: `${msg.id}-text-${producedCount}`,
107
+ content: [...textParts]
108
+ },
109
+ isFirstInGroup: isFirstInGroup && producedCount === 0
110
+ })
111
+ textParts.length = 0
112
+ producedCount++
113
+ }
114
+ }
115
+
116
+ for (const item of content) {
117
+ if (item.type === 'text') {
118
+ flushTools()
119
+ textParts.push(item)
120
+ } else if (item.type === 'tool_use') {
121
+ flushText()
122
+ const resultItem = findResult(item.id, i, messages)
123
+ currentToolGroup.push({ item, resultItem })
124
+ }
125
+ }
126
+
127
+ flushText()
128
+ flushTools()
129
+
130
+ // Handle legacy toolCall if no other content was produced
131
+ if (producedCount === 0 && msg.toolCall) {
132
+ result.push({
133
+ type: 'message',
134
+ message: msg,
135
+ isFirstInGroup
136
+ })
137
+ producedCount++
138
+ }
139
+
140
+ if (producedCount > 0) {
141
+ const lastItem = result[result.length - 1]
142
+ if (lastItem.type === 'tool-group') {
143
+ lastItem.footer = {
144
+ model: msg.model,
145
+ usage: msg.usage,
146
+ createdAt: msg.createdAt,
147
+ originalMessage: msg
148
+ }
149
+ }
150
+ }
151
+ }
152
+ i++
153
+ }
154
+
155
+ const mergedResult: ChatRenderItem[] = []
156
+ for (const item of result) {
157
+ if (item.type === 'tool-group') {
158
+ const prev = mergedResult[mergedResult.length - 1]
159
+ if (prev && prev.type === 'tool-group') {
160
+ prev.items.push(...item.items)
161
+ if (item.footer) {
162
+ prev.footer = item.footer
163
+ }
164
+ continue
165
+ }
166
+ }
167
+ mergedResult.push(item)
168
+ }
169
+
170
+ return mergedResult
171
+ }
@@ -0,0 +1,84 @@
1
+ export function toSerializable(value: unknown, seen = new WeakSet<object>()): unknown {
2
+ if (value == null) {
3
+ return value
4
+ }
5
+
6
+ const valueType = typeof value
7
+
8
+ if (valueType === 'bigint') {
9
+ return value.toString()
10
+ }
11
+
12
+ if (valueType === 'symbol') {
13
+ return value.toString()
14
+ }
15
+
16
+ if (valueType === 'function') {
17
+ return '[Function]'
18
+ }
19
+
20
+ if (valueType !== 'object') {
21
+ return value
22
+ }
23
+
24
+ if (value instanceof Date) {
25
+ return value.toISOString()
26
+ }
27
+
28
+ if (value instanceof Error) {
29
+ return {
30
+ name: value.name,
31
+ message: value.message,
32
+ stack: value.stack
33
+ }
34
+ }
35
+
36
+ if (value instanceof Map) {
37
+ return Array.from(value.entries()).map(([k, v]) => [toSerializable(k, seen), toSerializable(v, seen)])
38
+ }
39
+
40
+ if (value instanceof Set) {
41
+ return Array.from(value.values()).map(v => toSerializable(v, seen))
42
+ }
43
+
44
+ if (ArrayBuffer.isView(value)) {
45
+ return Array.from(value as unknown as ArrayLike<number>)
46
+ }
47
+
48
+ if (value instanceof ArrayBuffer) {
49
+ return Array.from(new Uint8Array(value))
50
+ }
51
+
52
+ if (Array.isArray(value)) {
53
+ return value.map(v => toSerializable(v, seen))
54
+ }
55
+
56
+ const obj = value as Record<string, unknown>
57
+ if (seen.has(obj)) {
58
+ return '[Circular]'
59
+ }
60
+ seen.add(obj)
61
+
62
+ if (typeof (obj as any).toJSON === 'function') {
63
+ try {
64
+ return toSerializable((obj as any).toJSON(), seen)
65
+ } catch {
66
+ return String(obj)
67
+ }
68
+ }
69
+
70
+ const output: Record<string, unknown> = {}
71
+ for (const [k, v] of Object.entries(obj)) {
72
+ output[k] = toSerializable(v, seen)
73
+ }
74
+ return output
75
+ }
76
+
77
+ export function safeJsonStringify(value: unknown, space = 2): string {
78
+ try {
79
+ const serialized = toSerializable(value)
80
+ return JSON.stringify(serialized, null, space) ?? 'null'
81
+ } catch {
82
+ return JSON.stringify(String(value)) ?? 'null'
83
+ }
84
+ }
@@ -0,0 +1,63 @@
1
+ import type { ChatMessageContent } from '@vibe-forge/core'
2
+ import React from 'react'
3
+ 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'
8
+
9
+ export function DefaultTool({
10
+ item,
11
+ resultItem
12
+ }: {
13
+ item: Extract<ChatMessageContent, { type: 'tool_use' }>
14
+ resultItem?: Extract<ChatMessageContent, { type: 'tool_result' }>
15
+ }) {
16
+ const { t } = useTranslation()
17
+ return (
18
+ <div className='tool-group'>
19
+ <ToolCallBox
20
+ 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
+ </>
28
+ }
29
+ content={
30
+ <div className='tool-content'>
31
+ <CodeBlock
32
+ code={safeJsonStringify(item.input != null ? item.input : {}, 2)}
33
+ lang='json'
34
+ />
35
+ </div>
36
+ }
37
+ />
38
+ {resultItem != null && (
39
+ <ToolCallBox
40
+ type='result'
41
+ isError={resultItem.is_error}
42
+ header={
43
+ <>
44
+ <span className='material-symbols-rounded' style={{ fontSize: 16 }}>
45
+ {resultItem.is_error === true ? 'error' : 'check_circle'}
46
+ </span>
47
+ <span>{t('chat.result')}</span>
48
+ </>
49
+ }
50
+ content={
51
+ <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' />}
57
+ </div>
58
+ }
59
+ />
60
+ )}
61
+ </div>
62
+ )
63
+ }
@@ -0,0 +1,71 @@
1
+ .tool-group.bash-tool {
2
+ .bash-header, .result-header {
3
+ display: flex;
4
+ align-items: center;
5
+ gap: 6px;
6
+
7
+ .status-icon {
8
+ font-size: 16px;
9
+ }
10
+
11
+ .bash-title, .result-title {
12
+ font-weight: 600;
13
+ }
14
+ }
15
+
16
+ .bash-command-preview {
17
+ font-family:
18
+ 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
19
+ font-size: 11px;
20
+ color: var(--text-color);
21
+ overflow: hidden;
22
+ text-overflow: ellipsis;
23
+ white-space: nowrap;
24
+ flex: 1;
25
+ }
26
+
27
+ .tool-reason {
28
+ margin: 0 0 4px 0;
29
+ font-size: 12px;
30
+ color: var(--sub-text-color);
31
+ line-height: 1.4;
32
+
33
+ &.markdown-body {
34
+ font-size: 12px;
35
+ color: inherit;
36
+ background-color: transparent;
37
+ padding: 0;
38
+
39
+ p {
40
+ margin: 0 !important;
41
+ padding: 0 !important;
42
+ }
43
+ }
44
+ }
45
+
46
+ .bash-content-scroll {
47
+ max-height: 400px;
48
+ overflow-y: auto;
49
+
50
+ .bash-code-wrapper {
51
+ background-color: transparent;
52
+ border: none;
53
+ border-radius: 0;
54
+ padding: 0;
55
+ }
56
+
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
+ }
70
+ }
71
+ }
@@ -0,0 +1,82 @@
1
+ import './BashTool.scss'
2
+ import React from 'react'
3
+ import { useTranslation } from 'react-i18next'
4
+ import ReactMarkdown from 'react-markdown'
5
+ import remarkGfm from 'remark-gfm'
6
+
7
+ import { defineToolRender } from '../defineToolRender'
8
+ import { CodeBlock } from '../../CodeBlock'
9
+ import { ToolCallBox } from '../../ToolCallBox'
10
+ import { safeJsonStringify } from '../../safeSerialize'
11
+
12
+ export const BashTool = defineToolRender(({ item, resultItem }) => {
13
+ const { t } = useTranslation()
14
+ const input = (item.input != null ? item.input : {}) as {
15
+ command?: string
16
+ reason?: string
17
+ thought?: string
18
+ description?: string
19
+ }
20
+ const command = input.command ?? ''
21
+ const reason = (input.description != null && input.description !== '')
22
+ ? input.description
23
+ : (input.reason != null && input.reason !== '')
24
+ ? input.reason
25
+ : (input.thought != null && input.thought !== '')
26
+ ? input.thought
27
+ : ''
28
+
29
+ return (
30
+ <div className='tool-group bash-tool'>
31
+ <ToolCallBox
32
+ collapsible={false}
33
+ header={
34
+ <div className='bash-header'>
35
+ <span className='material-symbols-rounded status-icon'>terminal</span>
36
+ <span className='bash-title'>{t('chat.tools.bash')}</span>
37
+ </div>
38
+ }
39
+ content={
40
+ <div className='tool-content'>
41
+ {(reason != null && reason !== '') && (
42
+ <div className='tool-reason markdown-body'>
43
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{reason}</ReactMarkdown>
44
+ </div>
45
+ )}
46
+ <div className='bash-content-scroll'>
47
+ <div className='bash-code-wrapper'>
48
+ <CodeBlock code={command} lang='shell' />
49
+ </div>
50
+ </div>
51
+ </div>
52
+ }
53
+ />
54
+
55
+ {resultItem != null && (
56
+ <ToolCallBox
57
+ type='result'
58
+ isError={resultItem.is_error}
59
+ header={
60
+ <div className='result-header'>
61
+ <span className='material-symbols-rounded status-icon'>
62
+ {resultItem.is_error === true ? 'error' : 'check_circle'}
63
+ </span>
64
+ <span className='result-title'>{t('chat.result')}</span>
65
+ </div>
66
+ }
67
+ content={
68
+ <div className='tool-content'>
69
+ <div className='bash-content-scroll'>
70
+ <div className='bash-code-wrapper'>
71
+ {typeof resultItem.content === 'string'
72
+ ? <CodeBlock code={resultItem.content} lang='text' />
73
+ : <CodeBlock code={safeJsonStringify(resultItem.content, 2)} lang='json' />}
74
+ </div>
75
+ </div>
76
+ </div>
77
+ }
78
+ />
79
+ )}
80
+ </div>
81
+ )
82
+ })
@@ -0,0 +1,88 @@
1
+ .glob-tool {
2
+ .tool-header-content {
3
+ display: flex;
4
+ align-items: center;
5
+ width: 100%;
6
+ height: 100%;
7
+ overflow: hidden;
8
+
9
+ .material-symbols-rounded {
10
+ font-size: 18px;
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ color: var(--sub-text-color);
15
+ height: 20px;
16
+ width: 18px;
17
+ margin-right: 8px;
18
+ position: relative;
19
+ top: 1px;
20
+ }
21
+
22
+ .command-name {
23
+ font-size: 13px;
24
+ font-weight: 500;
25
+ color: var(--text-color);
26
+ white-space: nowrap;
27
+ line-height: 20px;
28
+ display: flex;
29
+ align-items: center;
30
+ margin-right: 8px;
31
+ }
32
+
33
+ .pattern {
34
+ font-size: 12px;
35
+ color: var(--sub-text-color);
36
+ white-space: nowrap;
37
+ overflow: hidden;
38
+ text-overflow: ellipsis;
39
+ display: flex;
40
+ align-items: center;
41
+ min-width: 0;
42
+ flex: 1;
43
+ line-height: 20px;
44
+ }
45
+
46
+ .file-count {
47
+ font-size: 12px;
48
+ color: var(--sub-text-color);
49
+ white-space: nowrap;
50
+ margin-left: 8px;
51
+ flex-shrink: 0;
52
+ }
53
+ }
54
+
55
+ .input-details {
56
+ padding: 8px 12px;
57
+ font-size: 12px;
58
+ color: var(--sub-text-color);
59
+ border-bottom: 1px solid var(--border-color);
60
+
61
+ .label {
62
+ font-weight: 500;
63
+ margin-right: 8px;
64
+ }
65
+
66
+ .value {
67
+ font-family: var(--font-mono);
68
+ background: var(--bg-color-hover);
69
+ padding: 2px 4px;
70
+ border-radius: 4px;
71
+ }
72
+ }
73
+
74
+ .tool-placeholder {
75
+ padding: 8px 12px;
76
+ color: var(--sub-text-color);
77
+ font-size: 12px;
78
+ font-style: italic;
79
+ }
80
+
81
+ .file-list-container {
82
+ border-radius: 0;
83
+ border-left: none;
84
+ border-right: none;
85
+ border-bottom: none;
86
+ border: none;
87
+ }
88
+ }
@@ -0,0 +1,85 @@
1
+ import React, { useMemo } from 'react'
2
+ import './GlobTool.scss'
3
+ import { useTranslation } from 'react-i18next'
4
+
5
+ import { defineToolRender } from '../defineToolRender'
6
+ import { ToolCallBox } from '../../ToolCallBox'
7
+ import { safeJsonStringify } from '../../safeSerialize'
8
+ import { FileList } from './components/FileList'
9
+
10
+ export const GlobTool = defineToolRender(({ item, resultItem }) => {
11
+ const { t } = useTranslation()
12
+ const input = (item.input != null ? item.input : {}) as { pattern?: string; path?: string }
13
+ const pattern = (input.pattern != null && input.pattern !== '') ? input.pattern : '*'
14
+ const path = input.path
15
+
16
+ const fileCount = useMemo(() => {
17
+ if (!resultItem) return null
18
+ const content = resultItem.content
19
+ let lines: string[] = []
20
+
21
+ if (typeof content === 'string') {
22
+ if (content.trim().startsWith('[') && content.trim().endsWith(']')) {
23
+ try {
24
+ const parsed = JSON.parse(content)
25
+ if (Array.isArray(parsed)) {
26
+ lines = parsed.map(String)
27
+ } else {
28
+ lines = content.split('\n')
29
+ }
30
+ } catch (e) {
31
+ lines = content.split('\n')
32
+ }
33
+ } else {
34
+ lines = content.split('\n')
35
+ }
36
+ } else if (Array.isArray(content)) {
37
+ lines = content.map(String)
38
+ }
39
+
40
+ const count = lines.filter(line => line.trim() !== '').length
41
+ return count
42
+ }, [resultItem])
43
+
44
+ return (
45
+ <div className='tool-group glob-tool'>
46
+ <ToolCallBox
47
+ header={
48
+ <div className='tool-header-content'>
49
+ <span className='material-symbols-rounded'>search</span>
50
+ <span className='command-name'>Glob</span>
51
+ <span className='pattern'>{pattern}</span>
52
+ {fileCount !== null && (
53
+ <span className='file-count'>({fileCount} files)</span>
54
+ )}
55
+ </div>
56
+ }
57
+ content={
58
+ <div className='tool-content'>
59
+ {path && (
60
+ <div className='input-details'>
61
+ <span className='label'>Path:</span>
62
+ <span className='value'>{path}</span>
63
+ </div>
64
+ )}
65
+ {resultItem
66
+ ? (
67
+ <div className='result-content'>
68
+ <FileList
69
+ content={typeof resultItem.content === 'string'
70
+ ? resultItem.content
71
+ : safeJsonStringify(resultItem.content)}
72
+ />
73
+ </div>
74
+ )
75
+ : (
76
+ <div className='tool-placeholder'>
77
+ Searching...
78
+ </div>
79
+ )}
80
+ </div>
81
+ }
82
+ />
83
+ </div>
84
+ )
85
+ })